diff --git a/.github/workflows/staging-deployment.yaml b/.github/workflows/staging-deployment.yaml index 718eda47db..455ecbce8c 100644 --- a/.github/workflows/staging-deployment.yaml +++ b/.github/workflows/staging-deployment.yaml @@ -49,6 +49,6 @@ jobs: git pull make build-ee-query-service-amd64 make build-frontend-amd64 - make run-signoz + make run-testing EOF - gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}" + gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}" diff --git a/.github/workflows/testing-deployment.yaml b/.github/workflows/testing-deployment.yaml index b971eafb6d..f51de56192 100644 --- a/.github/workflows/testing-deployment.yaml +++ b/.github/workflows/testing-deployment.yaml @@ -50,6 +50,6 @@ jobs: git checkout --track origin/${GITHUB_BRANCH} make build-ee-query-service-amd64 make build-frontend-amd64 - make run-signoz + make run-testing EOF - gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}" + gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}" diff --git a/.gitignore b/.gitignore index 3f1834e9fa..8fe54dcf3d 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ ee/query-service/signoz.db ee/query-service/tests/test-deploy/data/ # local data +*.backup *.db /deploy/docker/clickhouse-setup/data/ /deploy/docker-swarm/clickhouse-setup/data/ @@ -61,4 +62,8 @@ e2e/test-results/ e2e/playwright-report/ e2e/blob-report/ e2e/playwright/.cache/ -e2e/.auth \ No newline at end of file +e2e/.auth + +# go +vendor/ +**/main/** diff --git a/.versions-golang b/.versions-golang deleted file mode 100644 index bc26b1c17f..0000000000 --- a/.versions-golang +++ /dev/null @@ -1,8 +0,0 @@ -#### Auto generated by make versions/golang. DO NOT EDIT! #### -amd64=128d7baad667abc0e41a85673026a2cf9449ef40f384baf424aee45bc13f9235 -arm=a5f77dc34ccae0d43269675508aab8fa9078ded6fa3e2dcee54f7c230018100d -arm64=1cdad16d01542a57caca4b0a6893a5b69d711d69dd6bb4483c77c1d092baec41 -386=0c82e5195d14caa5daa01ea06a70139e7ea1edbd366c83259227c7d9965d4c5a -mips64le=25967f27f76031f31cd3ae2173958e151d8d961ca186ab4328af7a1895139a66 -ppc64le=6fa49b4730622b79560a1fc2677b02a1ee7aac5b28490a2bda6134050108fb3a -s390x=4e2c0198c3db1c769e8e2e8a1e504dbb5e3eff0dad62f8f5c543b4823a89d81b diff --git a/Makefile b/Makefile index 5213c4597a..95cf7afdb9 100644 --- a/Makefile +++ b/Makefile @@ -156,6 +156,9 @@ pull-signoz: run-signoz: @docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml up --build -d +run-testing: + @docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.testing.yaml up --build -d + down-signoz: @docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml down -v diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index e6354bb35e..755d61c919 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -146,7 +146,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.46.0 + image: signoz/query-service:0.47.0 command: [ "-config=/root/config/prometheus.yml", @@ -186,7 +186,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:0.46.0 + image: signoz/frontend:0.47.0 deploy: restart_policy: condition: on-failure @@ -199,7 +199,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.88.24 + image: signoz/signoz-otel-collector:0.88.26 command: [ "--config=/etc/otel-collector-config.yaml", @@ -237,7 +237,7 @@ services: - query-service otel-collector-migrator: - image: signoz/signoz-schema-migrator:0.88.24 + image: signoz/signoz-schema-migrator:0.88.26 deploy: restart_policy: condition: on-failure diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index cf1e5f1ed4..d18c10f913 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -66,7 +66,7 @@ services: - --storage.path=/data otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.26} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -81,7 +81,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` otel-collector: container_name: signoz-otel-collector - image: signoz/signoz-otel-collector:0.88.24 + image: signoz/signoz-otel-collector:0.88.26 command: [ "--config=/etc/otel-collector-config.yaml", diff --git a/deploy/docker/clickhouse-setup/docker-compose.testing.yaml b/deploy/docker/clickhouse-setup/docker-compose.testing.yaml new file mode 100644 index 0000000000..48d77b98df --- /dev/null +++ b/deploy/docker/clickhouse-setup/docker-compose.testing.yaml @@ -0,0 +1,307 @@ +version: "2.4" + +x-clickhouse-defaults: &clickhouse-defaults + restart: on-failure + # addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab + image: clickhouse/clickhouse-server:24.1.2-alpine + tty: true + depends_on: + - zookeeper-1 + # - zookeeper-2 + # - zookeeper-3 + logging: + options: + max-size: 50m + max-file: "3" + healthcheck: + # "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'" + test: + [ + "CMD", + "wget", + "--spider", + "-q", + "0.0.0.0:8123/ping" + ] + interval: 30s + timeout: 5s + retries: 3 + ulimits: + nproc: 65535 + nofile: + soft: 262144 + hard: 262144 + +x-db-depend: &db-depend + depends_on: + clickhouse: + condition: service_healthy + otel-collector-migrator: + condition: service_completed_successfully + # clickhouse-2: + # condition: service_healthy + # clickhouse-3: + # condition: service_healthy + +services: + + zookeeper-1: + image: bitnami/zookeeper:3.7.1 + container_name: signoz-zookeeper-1 + hostname: zookeeper-1 + user: root + ports: + - "2181:2181" + - "2888:2888" + - "3888:3888" + volumes: + - ./data/zookeeper-1:/bitnami/zookeeper + environment: + - ZOO_SERVER_ID=1 + # - ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888 + - ALLOW_ANONYMOUS_LOGIN=yes + - ZOO_AUTOPURGE_INTERVAL=1 + + # zookeeper-2: + # image: bitnami/zookeeper:3.7.0 + # container_name: signoz-zookeeper-2 + # hostname: zookeeper-2 + # user: root + # ports: + # - "2182:2181" + # - "2889:2888" + # - "3889:3888" + # volumes: + # - ./data/zookeeper-2:/bitnami/zookeeper + # environment: + # - ZOO_SERVER_ID=2 + # - ZOO_SERVERS=zookeeper-1:2888:3888,0.0.0.0:2888:3888,zookeeper-3:2888:3888 + # - ALLOW_ANONYMOUS_LOGIN=yes + # - ZOO_AUTOPURGE_INTERVAL=1 + + # zookeeper-3: + # image: bitnami/zookeeper:3.7.0 + # container_name: signoz-zookeeper-3 + # hostname: zookeeper-3 + # user: root + # ports: + # - "2183:2181" + # - "2890:2888" + # - "3890:3888" + # volumes: + # - ./data/zookeeper-3:/bitnami/zookeeper + # environment: + # - ZOO_SERVER_ID=3 + # - ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888 + # - ALLOW_ANONYMOUS_LOGIN=yes + # - ZOO_AUTOPURGE_INTERVAL=1 + + clickhouse: + <<: *clickhouse-defaults + container_name: signoz-clickhouse + hostname: clickhouse + ports: + - "9000:9000" + - "8123:8123" + - "9181:9181" + volumes: + - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml + - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml + - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml + - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml + # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml + - ./data/clickhouse/:/var/lib/clickhouse/ + - ./user_scripts:/var/lib/clickhouse/user_scripts/ + + # clickhouse-2: + # <<: *clickhouse-defaults + # container_name: signoz-clickhouse-2 + # hostname: clickhouse-2 + # ports: + # - "9001:9000" + # - "8124:8123" + # - "9182:9181" + # volumes: + # - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml + # - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml + # - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml + # - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml + # # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml + # - ./data/clickhouse-2/:/var/lib/clickhouse/ + # - ./user_scripts:/var/lib/clickhouse/user_scripts/ + + + # clickhouse-3: + # <<: *clickhouse-defaults + # container_name: signoz-clickhouse-3 + # hostname: clickhouse-3 + # ports: + # - "9002:9000" + # - "8125:8123" + # - "9183:9181" + # volumes: + # - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml + # - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml + # - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml + # - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml + # # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml + # - ./data/clickhouse-3/:/var/lib/clickhouse/ + # - ./user_scripts:/var/lib/clickhouse/user_scripts/ + + alertmanager: + image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5} + container_name: signoz-alertmanager + volumes: + - ./data/alertmanager:/data + depends_on: + query-service: + condition: service_healthy + restart: on-failure + command: + - --queryService.url=http://query-service:8085 + - --storage.path=/data + + # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` + + query-service: + image: signoz/query-service:${DOCKER_TAG:-0.47.0} + container_name: signoz-query-service + command: + [ + "-config=/root/config/prometheus.yml", + "-gateway-url=https://api.staging.signoz.cloud" + # "--prefer-delta=true" + ] + # ports: + # - "6060:6060" # pprof port + # - "8080:8080" # query-service port + volumes: + - ./prometheus.yml:/root/config/prometheus.yml + - ../dashboards:/root/config/dashboards + - ./data/signoz/:/var/lib/signoz/ + environment: + - ClickHouseUrl=tcp://clickhouse:9000 + - ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/ + - SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db + - DASHBOARDS_PATH=/root/config/dashboards + - STORAGE=clickhouse + - GODEBUG=netdns=go + - TELEMETRY_ENABLED=true + - DEPLOYMENT_TYPE=docker-standalone-amd + restart: on-failure + healthcheck: + test: + [ + "CMD", + "wget", + "--spider", + "-q", + "localhost:8080/api/v1/health" + ] + interval: 30s + timeout: 5s + retries: 3 + <<: *db-depend + + frontend: + image: signoz/frontend:${DOCKER_TAG:-0.47.0} + container_name: signoz-frontend + restart: on-failure + depends_on: + - alertmanager + - query-service + ports: + - "3301:3301" + volumes: + - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf + + otel-collector-migrator: + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.26} + container_name: otel-migrator + command: + - "--dsn=tcp://clickhouse:9000" + depends_on: + clickhouse: + condition: service_healthy + # clickhouse-2: + # condition: service_healthy + # clickhouse-3: + # condition: service_healthy + + + otel-collector: + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.26} + container_name: signoz-otel-collector + command: + [ + "--config=/etc/otel-collector-config.yaml", + "--manager-config=/etc/manager-config.yaml", + "--copy-path=/var/tmp/collector-config.yaml", + "--feature-gates=-pkg.translator.prometheus.NormalizeName" + ] + user: root # required for reading docker container logs + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml + - ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml + - /var/lib/docker/containers:/var/lib/docker/containers:ro + environment: + - OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux + - DOCKER_MULTI_NODE_CLUSTER=false + - LOW_CARDINAL_EXCEPTION_GROUPING=false + ports: + # - "1777:1777" # pprof extension + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + # - "8888:8888" # OtelCollector internal metrics + # - "8889:8889" # signoz spanmetrics exposed by the agent + # - "9411:9411" # Zipkin port + # - "13133:13133" # health check extension + # - "14250:14250" # Jaeger gRPC + # - "14268:14268" # Jaeger thrift HTTP + # - "55678:55678" # OpenCensus receiver + # - "55679:55679" # zPages extension + restart: on-failure + depends_on: + clickhouse: + condition: service_healthy + otel-collector-migrator: + condition: service_completed_successfully + query-service: + condition: service_healthy + + logspout: + image: "gliderlabs/logspout:v3.2.14" + container_name: signoz-logspout + volumes: + - /etc/hostname:/etc/host_hostname:ro + - /var/run/docker.sock:/var/run/docker.sock + command: syslog+tcp://otel-collector:2255 + depends_on: + - otel-collector + restart: on-failure + + hotrod: + image: jaegertracing/example-hotrod:1.30 + container_name: hotrod + logging: + options: + max-size: 50m + max-file: "3" + command: [ "all" ] + environment: + - JAEGER_ENDPOINT=http://otel-collector:14268/api/traces + + load-hotrod: + image: "signoz/locust:1.2.3" + container_name: load-hotrod + hostname: load-hotrod + environment: + ATTACKED_HOST: http://hotrod:8080 + LOCUST_MODE: standalone + NO_PROXY: standalone + TASK_DELAY_FROM: 5 + TASK_DELAY_TO: 30 + QUIET_MODE: "${QUIET_MODE:-false}" + LOCUST_OPTS: "--headless -u 10 -r 1" + volumes: + - ../common/locust-scripts:/locust diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 12b91b6992..d47b7acd46 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -164,11 +164,11 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` query-service: - image: signoz/query-service:${DOCKER_TAG:-0.46.0} + image: signoz/query-service:${DOCKER_TAG:-0.47.0} container_name: signoz-query-service command: [ - "-config=/root/config/prometheus.yml", + "-config=/root/config/prometheus.yml" # "--prefer-delta=true" ] # ports: @@ -203,7 +203,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.46.0} + image: signoz/frontend:${DOCKER_TAG:-0.47.0} container_name: signoz-frontend restart: on-failure depends_on: @@ -215,7 +215,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.26} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -229,7 +229,7 @@ services: otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.24} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.26} container_name: signoz-otel-collector command: [ diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index 418cd00cf9..be0cf1ec36 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -2,10 +2,12 @@ package api import ( "net/http" + "net/http/httputil" "time" "github.com/gorilla/mux" "go.signoz.io/signoz/ee/query-service/dao" + "go.signoz.io/signoz/ee/query-service/integrations/gateway" "go.signoz.io/signoz/ee/query-service/interfaces" "go.signoz.io/signoz/ee/query-service/license" "go.signoz.io/signoz/ee/query-service/usage" @@ -35,6 +37,7 @@ type APIHandlerOptions struct { IntegrationsController *integrations.Controller LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController Cache cache.Cache + Gateway *httputil.ReverseProxy // Querier Influx Interval FluxInterval time.Duration } @@ -95,6 +98,10 @@ func (ah *APIHandler) AppDao() dao.ModelDao { return ah.opts.AppDao } +func (ah *APIHandler) Gateway() *httputil.ReverseProxy { + return ah.opts.Gateway +} + func (ah *APIHandler) CheckFeature(f string) bool { err := ah.FF().CheckFeature(f) return err == nil @@ -170,6 +177,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew am.ViewAccess(ah.listLicensesV2)). Methods(http.MethodGet) + // Gateway + router.PathPrefix(gateway.RoutePrefix).HandlerFunc(am.AdminAccess(ah.ServeGatewayHTTP)) + ah.APIHandler.RegisterRoutes(router, am) } diff --git a/ee/query-service/app/api/dashboard.go b/ee/query-service/app/api/dashboard.go index 83c82a1477..0628ae18f6 100644 --- a/ee/query-service/app/api/dashboard.go +++ b/ee/query-service/app/api/dashboard.go @@ -1,12 +1,13 @@ package api import ( + "net/http" + "github.com/gorilla/mux" "go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/common" "go.signoz.io/signoz/pkg/query-service/model" - "net/http" ) func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) { diff --git a/ee/query-service/app/api/gateway.go b/ee/query-service/app/api/gateway.go new file mode 100644 index 0000000000..15d274ee23 --- /dev/null +++ b/ee/query-service/app/api/gateway.go @@ -0,0 +1,34 @@ +package api + +import ( + "net/http" + "strings" + + "go.signoz.io/signoz/ee/query-service/integrations/gateway" +) + +func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + if !strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+gateway.AllowedPrefix) { + rw.WriteHeader(http.StatusNotFound) + return + } + + license, err := ah.LM().GetRepo().GetActiveLicense(ctx) + if err != nil { + RespondError(rw, err, nil) + return + } + + //Create headers + var licenseKey string + if license != nil { + licenseKey = license.Key + } + + req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey) + req.Header.Set("X-Consumer-Username", "lid:00000000-0000-0000-0000-000000000000") + req.Header.Set("X-Consumer-Groups", "ns:default") + + ah.Gateway().ServeHTTP(rw, req) +} diff --git a/ee/query-service/app/api/traces.go b/ee/query-service/app/api/traces.go index ee18b2f50b..3864fc672e 100644 --- a/ee/query-service/app/api/traces.go +++ b/ee/query-service/app/api/traces.go @@ -2,10 +2,8 @@ package api import ( "net/http" - "strconv" "go.signoz.io/signoz/ee/query-service/app/db" - "go.signoz.io/signoz/ee/query-service/constants" "go.signoz.io/signoz/ee/query-service/model" baseapp "go.signoz.io/signoz/pkg/query-service/app" basemodel "go.signoz.io/signoz/pkg/query-service/model" @@ -19,17 +17,13 @@ func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) { ah.APIHandler.SearchTraces(w, r) return } - traceId, spanId, levelUpInt, levelDownInt, err := baseapp.ParseSearchTracesParams(r) + searchTracesParams, err := baseapp.ParseSearchTracesParams(r) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading params") return } - spanLimit, err := strconv.Atoi(constants.SpanLimitStr) - if err != nil { - zap.L().Error("Error during strconv.Atoi() on SPAN_LIMIT env variable", zap.Error(err)) - return - } - result, err := ah.opts.DataConnector.SearchTraces(r.Context(), traceId, spanId, levelUpInt, levelDownInt, spanLimit, db.SmartTraceAlgorithm) + + result, err := ah.opts.DataConnector.SearchTraces(r.Context(), searchTracesParams, db.SmartTraceAlgorithm) if ah.HandleError(w, err, http.StatusBadRequest) { return } diff --git a/ee/query-service/app/db/trace.go b/ee/query-service/app/db/trace.go index c6fe9045cf..dec222a09c 100644 --- a/ee/query-service/app/db/trace.go +++ b/ee/query-service/app/db/trace.go @@ -13,6 +13,11 @@ import ( func SmartTraceAlgorithm(payload []basemodel.SearchSpanResponseItem, targetSpanId string, levelUp int, levelDown int, spanLimit int) ([]basemodel.SearchSpansResult, error) { var spans []*model.SpanForTraceDetails + // if targetSpanId is null or not present then randomly select a span as targetSpanId + if (targetSpanId == "" || targetSpanId == "null") && len(payload) > 0 { + targetSpanId = payload[0].SpanID + } + // Build a slice of spans from the payload for _, spanItem := range payload { var parentID string @@ -115,6 +120,7 @@ func SmartTraceAlgorithm(payload []basemodel.SearchSpanResponseItem, targetSpanI searchSpansResult := []basemodel.SearchSpansResult{{ Columns: []string{"__time", "SpanId", "TraceId", "ServiceName", "Name", "Kind", "DurationNano", "TagsKeys", "TagsValues", "References", "Events", "HasError"}, Events: make([][]interface{}, len(resultSpansSet)), + IsSubTree: true, }, } diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 53b9a27314..2e1df484d1 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -8,6 +8,7 @@ import ( "io" "net" "net/http" + "net/http/httputil" _ "net/http/pprof" // http profiler "os" "regexp" @@ -24,6 +25,7 @@ import ( "go.signoz.io/signoz/ee/query-service/auth" "go.signoz.io/signoz/ee/query-service/constants" "go.signoz.io/signoz/ee/query-service/dao" + "go.signoz.io/signoz/ee/query-service/integrations/gateway" "go.signoz.io/signoz/ee/query-service/interfaces" baseauth "go.signoz.io/signoz/pkg/query-service/auth" baseInterface "go.signoz.io/signoz/pkg/query-service/interfaces" @@ -71,6 +73,7 @@ type ServerOptions struct { CacheConfigPath string FluxInterval string Cluster string + GatewayUrl string } // Server runs HTTP api service @@ -122,8 +125,33 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { localDB.SetMaxOpenConns(10) + gatewayFeature := basemodel.Feature{ + Name: "GATEWAY", + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + } + + //Activate this feature if the url is not empty + var gatewayProxy *httputil.ReverseProxy + if serverOptions.GatewayUrl == "" { + gatewayFeature.Active = false + gatewayProxy, err = gateway.NewNoopProxy() + if err != nil { + return nil, err + } + } else { + zap.L().Info("Enabling gateway feature flag ...") + gatewayFeature.Active = true + gatewayProxy, err = gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix) + if err != nil { + return nil, err + } + } + // initiate license manager - lm, err := licensepkg.StartManager("sqlite", localDB) + lm, err := licensepkg.StartManager("sqlite", localDB, gatewayFeature) if err != nil { return nil, err } @@ -248,6 +276,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { LogsParsingPipelineController: logParsingPipelineController, Cache: c, FluxInterval: fluxInterval, + Gateway: gatewayProxy, } apiHandler, err := api.NewAPIHandler(apiOpts) @@ -710,6 +739,7 @@ func makeRulesManager( Logger: nil, DisableRules: disableRules, FeatureFlags: fm, + Reader: ch, } // create Manager diff --git a/ee/query-service/constants/constants.go b/ee/query-service/constants/constants.go index aeeea03cf2..cc4bb07476 100644 --- a/ee/query-service/constants/constants.go +++ b/ee/query-service/constants/constants.go @@ -11,7 +11,8 @@ const ( var LicenseSignozIo = "https://license.signoz.io/api/v1" var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "") var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "") -var SpanLimitStr = GetOrDefaultEnv("SPAN_LIMIT", "5000") +var SpanRenderLimitStr = GetOrDefaultEnv("SPAN_RENDER_LIMIT", "2500") +var MaxSpansInTraceStr = GetOrDefaultEnv("MAX_SPANS_IN_TRACE", "250000") func GetOrDefaultEnv(key string, fallback string) string { v := os.Getenv(key) diff --git a/ee/query-service/dao/interface.go b/ee/query-service/dao/interface.go index 695ff860a0..2fc81468d5 100644 --- a/ee/query-service/dao/interface.go +++ b/ee/query-service/dao/interface.go @@ -34,7 +34,7 @@ type ModelDao interface { GetDomainByEmail(ctx context.Context, email string) (*model.OrgDomain, basemodel.BaseApiError) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basemodel.BaseApiError) - UpdatePAT(ctx context.Context, p model.PAT, id string) (basemodel.BaseApiError) + UpdatePAT(ctx context.Context, p model.PAT, id string) basemodel.BaseApiError GetPAT(ctx context.Context, pat string) (*model.PAT, basemodel.BaseApiError) UpdatePATLastUsed(ctx context.Context, pat string, lastUsed int64) basemodel.BaseApiError GetPATByID(ctx context.Context, id string) (*model.PAT, basemodel.BaseApiError) diff --git a/ee/query-service/integrations/gateway/noop.go b/ee/query-service/integrations/gateway/noop.go new file mode 100644 index 0000000000..bbe930e2f9 --- /dev/null +++ b/ee/query-service/integrations/gateway/noop.go @@ -0,0 +1,9 @@ +package gateway + +import ( + "net/http/httputil" +) + +func NewNoopProxy() (*httputil.ReverseProxy, error) { + return nil, nil +} diff --git a/ee/query-service/integrations/gateway/proxy.go b/ee/query-service/integrations/gateway/proxy.go new file mode 100644 index 0000000000..8b225c4459 --- /dev/null +++ b/ee/query-service/integrations/gateway/proxy.go @@ -0,0 +1,66 @@ +package gateway + +import ( + "net/http" + "net/http/httputil" + "net/url" + "path" + "strings" +) + +const ( + RoutePrefix string = "/api/gateway" + AllowedPrefix string = "/v1/workspaces/me" +) + +type proxy struct { + url *url.URL + stripPath string +} + +func NewProxy(u string, stripPath string) (*httputil.ReverseProxy, error) { + url, err := url.Parse(u) + if err != nil { + return nil, err + } + + proxy := &proxy{url: url, stripPath: stripPath} + + return &httputil.ReverseProxy{ + Rewrite: proxy.rewrite, + ModifyResponse: proxy.modifyResponse, + ErrorHandler: proxy.errorHandler, + }, nil +} + +func (p *proxy) rewrite(pr *httputil.ProxyRequest) { + pr.SetURL(p.url) + pr.SetXForwarded() + pr.Out.URL.Path = cleanPath(strings.ReplaceAll(pr.Out.URL.Path, p.stripPath, "")) +} + +func (p *proxy) modifyResponse(res *http.Response) error { + return nil +} + +func (p *proxy) errorHandler(rw http.ResponseWriter, req *http.Request, err error) { + rw.WriteHeader(http.StatusBadGateway) +} + +func cleanPath(p string) string { + if p == "" { + return "/" + } + if p[0] != '/' { + p = "/" + p + } + np := path.Clean(p) + if p[len(p)-1] == '/' && np != "/" { + if len(p) == len(np)+1 && strings.HasPrefix(p, np) { + np = p + } else { + np += "/" + } + } + return np +} diff --git a/ee/query-service/integrations/gateway/proxy_test.go b/ee/query-service/integrations/gateway/proxy_test.go new file mode 100644 index 0000000000..45f5211efe --- /dev/null +++ b/ee/query-service/integrations/gateway/proxy_test.go @@ -0,0 +1,61 @@ +package gateway + +import ( + "context" + "net/http" + "net/http/httputil" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProxyRewrite(t *testing.T) { + testCases := []struct { + name string + url *url.URL + stripPath string + in *url.URL + expected *url.URL + }{ + { + name: "SamePathAdded", + url: &url.URL{Scheme: "http", Host: "backend", Path: "/path1"}, + stripPath: "/strip", + in: &url.URL{Scheme: "http", Host: "localhost", Path: "/strip/path1"}, + expected: &url.URL{Scheme: "http", Host: "backend", Path: "/path1/path1"}, + }, + { + name: "NoStripPathInput", + url: &url.URL{Scheme: "http", Host: "backend"}, + stripPath: "", + in: &url.URL{Scheme: "http", Host: "localhost", Path: "/strip/path1"}, + expected: &url.URL{Scheme: "http", Host: "backend", Path: "/strip/path1"}, + }, + { + name: "NoStripPathPresentInReq", + url: &url.URL{Scheme: "http", Host: "backend"}, + stripPath: "/not-found", + in: &url.URL{Scheme: "http", Host: "localhost", Path: "/strip/path1"}, + expected: &url.URL{Scheme: "http", Host: "backend", Path: "/strip/path1"}, + }, + } + + for _, tc := range testCases { + proxy, err := NewProxy(tc.url.String(), tc.stripPath) + require.NoError(t, err) + inReq, err := http.NewRequest(http.MethodGet, tc.in.String(), nil) + require.NoError(t, err) + proxyReq := &httputil.ProxyRequest{ + In: inReq, + Out: inReq.Clone(context.Background()), + } + proxy.Rewrite(proxyReq) + + assert.Equal(t, tc.expected.Host, proxyReq.Out.URL.Host) + assert.Equal(t, tc.expected.Scheme, proxyReq.Out.URL.Scheme) + assert.Equal(t, tc.expected.Path, proxyReq.Out.URL.Path) + assert.Equal(t, tc.expected.Query(), proxyReq.Out.URL.Query()) + } +} diff --git a/ee/query-service/license/db.go b/ee/query-service/license/db.go index bf71e9376d..d6065d045b 100644 --- a/ee/query-service/license/db.go +++ b/ee/query-service/license/db.go @@ -48,8 +48,9 @@ func (r *Repo) GetLicenses(ctx context.Context) ([]model.License, error) { return licenses, nil } -// GetActiveLicense fetches the latest active license from DB -func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, error) { +// GetActiveLicense fetches the latest active license from DB. +// If the license is not present, expect a nil license and a nil error in the output. +func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) { var err error licenses := []model.License{} @@ -57,7 +58,7 @@ func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, error) { err = r.db.Select(&licenses, query) if err != nil { - return nil, fmt.Errorf("failed to get active licenses from db: %v", err) + return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err)) } var active *model.License diff --git a/ee/query-service/license/manager.go b/ee/query-service/license/manager.go index d348b6d216..74887608ab 100644 --- a/ee/query-service/license/manager.go +++ b/ee/query-service/license/manager.go @@ -49,8 +49,7 @@ type Manager struct { activeFeatures basemodel.FeatureSet } -func StartManager(dbType string, db *sqlx.DB) (*Manager, error) { - +func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) { if LM != nil { return LM, nil } @@ -66,7 +65,7 @@ func StartManager(dbType string, db *sqlx.DB) (*Manager, error) { repo: &repo, } - if err := m.start(); err != nil { + if err := m.start(features...); err != nil { return m, err } LM = m @@ -74,8 +73,8 @@ func StartManager(dbType string, db *sqlx.DB) (*Manager, error) { } // start loads active license in memory and initiates validator -func (lm *Manager) start() error { - err := lm.LoadActiveLicense() +func (lm *Manager) start(features ...basemodel.Feature) error { + err := lm.LoadActiveLicense(features...) return err } @@ -85,7 +84,7 @@ func (lm *Manager) Stop() { <-lm.terminated } -func (lm *Manager) SetActive(l *model.License) { +func (lm *Manager) SetActive(l *model.License, features ...basemodel.Feature) { lm.mutex.Lock() defer lm.mutex.Unlock() @@ -94,7 +93,7 @@ func (lm *Manager) SetActive(l *model.License) { } lm.activeLicense = l - lm.activeFeatures = l.FeatureSet + lm.activeFeatures = append(l.FeatureSet, features...) // set default features setDefaultFeatures(lm) @@ -116,14 +115,13 @@ func setDefaultFeatures(lm *Manager) { } // LoadActiveLicense loads the most recent active license -func (lm *Manager) LoadActiveLicense() error { - var err error +func (lm *Manager) LoadActiveLicense(features ...basemodel.Feature) error { active, err := lm.repo.GetActiveLicense(context.Background()) if err != nil { return err } if active != nil { - lm.SetActive(active) + lm.SetActive(active, features...) } else { zap.L().Info("No active license found, defaulting to basic plan") // if no active license is found, we default to basic(free) plan with all default features diff --git a/ee/query-service/main.go b/ee/query-service/main.go index 4fad91008f..f88f2cb498 100644 --- a/ee/query-service/main.go +++ b/ee/query-service/main.go @@ -95,6 +95,7 @@ func main() { var maxIdleConns int var maxOpenConns int var dialTimeout time.Duration + var gatewayUrl string flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)") flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)") @@ -109,6 +110,7 @@ func main() { flag.StringVar(&fluxInterval, "flux-interval", "5m", "(cache config to use)") flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)") flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')") + flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)") flag.Parse() @@ -134,6 +136,7 @@ func main() { CacheConfigPath: cacheConfigPath, FluxInterval: fluxInterval, Cluster: cluster, + GatewayUrl: gatewayUrl, } // Read the jwt secret key diff --git a/frontend/package.json b/frontend/package.json index a433b698e2..25d32f69df 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -85,9 +85,10 @@ "less": "^4.1.2", "less-loader": "^10.2.0", "lodash-es": "^4.17.21", - "lucide-react": "0.321.0", + "lucide-react": "0.379.0", "mini-css-extract-plugin": "2.4.5", "papaparse": "5.4.1", + "rc-tween-one": "3.0.6", "react": "18.2.0", "react-addons-update": "15.6.3", "react-beautiful-dnd": "13.1.1", @@ -235,6 +236,7 @@ "@types/react-dom": "18.0.10", "debug": "4.3.4", "semver": "7.5.4", - "xml2js": "0.5.0" + "xml2js": "0.5.0", + "phin": "^3.7.1" } } diff --git a/frontend/public/Icons/dashboard_emoji.svg b/frontend/public/Icons/dashboard_emoji.svg new file mode 100644 index 0000000000..67d99d6e13 --- /dev/null +++ b/frontend/public/Icons/dashboard_emoji.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/Icons/dashboards.svg b/frontend/public/Icons/dashboards.svg new file mode 100644 index 0000000000..88386b138c --- /dev/null +++ b/frontend/public/Icons/dashboards.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/Icons/landscape.svg b/frontend/public/Icons/landscape.svg new file mode 100644 index 0000000000..762d34523a --- /dev/null +++ b/frontend/public/Icons/landscape.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/Icons/tools.svg b/frontend/public/Icons/tools.svg new file mode 100644 index 0000000000..f4d33bc245 --- /dev/null +++ b/frontend/public/Icons/tools.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/Images/blankDashboardTemplatePreview.svg b/frontend/public/Images/blankDashboardTemplatePreview.svg new file mode 100644 index 0000000000..5c93cf3dfa --- /dev/null +++ b/frontend/public/Images/blankDashboardTemplatePreview.svg @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/Images/redisTemplatePreview.svg b/frontend/public/Images/redisTemplatePreview.svg new file mode 100644 index 0000000000..aed4e97755 --- /dev/null +++ b/frontend/public/Images/redisTemplatePreview.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/public/locales/en-GB/dashboard.json b/frontend/public/locales/en-GB/dashboard.json index 49a0ff39dd..ec804ccfe8 100644 --- a/frontend/public/locales/en-GB/dashboard.json +++ b/frontend/public/locales/en-GB/dashboard.json @@ -1,6 +1,6 @@ { "create_dashboard": "Create Dashboard", - "import_json": "Import JSON", + "import_json": "Import Dashboard JSON", "import_grafana_json": "Import Grafana JSON", "copy_to_clipboard": "Copy To ClipBoard", "download_json": "Download JSON", @@ -9,7 +9,7 @@ "upload_json_file": "Upload JSON file", "paste_json_below": "Paste JSON below", "error_upload_json": "Invalid JSON", - "load_json": "Load JSON", + "import_and_next": "Import and Next", "import_dashboard_by_pasting": "Import dashboard by pasting JSON or importing JSON file", "error_loading_json": "Error loading JSON file", "empty_json_not_allowed": "Empty JSON is not allowed", diff --git a/frontend/public/locales/en-GB/ingestionKeys.json b/frontend/public/locales/en-GB/ingestionKeys.json new file mode 100644 index 0000000000..256e88391a --- /dev/null +++ b/frontend/public/locales/en-GB/ingestionKeys.json @@ -0,0 +1,3 @@ +{ + "delete_confirm_message": "Are you sure you want to delete {{keyName}}? Deleting an ingestion key is irreversible and cannot be undone." +} diff --git a/frontend/public/locales/en/dashboard.json b/frontend/public/locales/en/dashboard.json index c214c027c2..d2e90237a9 100644 --- a/frontend/public/locales/en/dashboard.json +++ b/frontend/public/locales/en/dashboard.json @@ -1,6 +1,6 @@ { "create_dashboard": "Create Dashboard", - "import_json": "Import JSON", + "import_json": "Import Dashboard JSON", "import_grafana_json": "Import Grafana JSON", "copy_to_clipboard": "Copy To ClipBoard", "download_json": "Download JSON", @@ -9,7 +9,7 @@ "upload_json_file": "Upload JSON file", "paste_json_below": "Paste JSON below", "error_upload_json": "Invalid JSON", - "load_json": "Load JSON", + "import_and_next": "Import and Next", "import_dashboard_by_pasting": "Import dashboard by pasting JSON or importing JSON file", "error_loading_json": "Error loading JSON file", "empty_json_not_allowed": "Empty JSON is not allowed", diff --git a/frontend/public/locales/en/ingestionKeys.json b/frontend/public/locales/en/ingestionKeys.json new file mode 100644 index 0000000000..58ebf8a0d9 --- /dev/null +++ b/frontend/public/locales/en/ingestionKeys.json @@ -0,0 +1,4 @@ +{ + "delete_confirm_message": "Are you sure you want to delete {{keyName}}? Deleting an ingestion key is irreversible and cannot be undone.", + "delete_limit_confirm_message": "Are you sure you want to delete {{limit_name}} limit for ingestion key {{keyName}}?" +} diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index 7bc58c509e..945ac8b6be 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -9,13 +9,14 @@ import ROUTES from 'constants/routes'; import AppLayout from 'container/AppLayout'; import useAnalytics from 'hooks/analytics/useAnalytics'; import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys'; -import { useThemeConfig } from 'hooks/useDarkMode'; +import { useIsDarkMode, useThemeConfig } from 'hooks/useDarkMode'; +import { THEME_MODE } from 'hooks/useDarkMode/constant'; import useGetFeatureFlag from 'hooks/useGetFeatureFlag'; import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense'; import { NotificationProvider } from 'hooks/useNotifications'; import { ResourceProvider } from 'hooks/useResourceAttribute'; import history from 'lib/history'; -import { identity, pickBy } from 'lodash-es'; +import { identity, pick, pickBy } from 'lodash-es'; import { DashboardProvider } from 'providers/Dashboard/Dashboard'; import { QueryBuilderProvider } from 'providers/QueryBuilder'; import { Suspense, useEffect, useState } from 'react'; @@ -46,12 +47,14 @@ function App(): JSX.Element { const dispatch = useDispatch>(); - const { trackPageView } = useAnalytics(); + const { trackPageView, trackEvent } = useAnalytics(); const { hostname, pathname } = window.location; const isCloudUserVal = isCloudUser(); + const isDarkMode = useIsDarkMode(); + const featureResponse = useGetFeatureFlag((allFlags) => { const isOnboardingEnabled = allFlags.find((flag) => flag.name === FeatureKeys.ONBOARDING)?.active || @@ -174,6 +177,25 @@ function App(): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathname]); + useEffect(() => { + try { + const isThemeAnalyticsSent = getLocalStorageApi( + LOCALSTORAGE.THEME_ANALYTICS, + ); + if (!isThemeAnalyticsSent) { + trackEvent('Theme Analytics', { + theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT, + user: pick(user, ['email', 'userId', 'name']), + org, + }); + setLocalStorageApi(LOCALSTORAGE.THEME_ANALYTICS, 'true'); + } + } catch { + console.error('Failed to parse local storage theme analytics event'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 1252496c08..bda390afbf 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -7,7 +7,7 @@ export const ServicesTablePage = Loadable( export const ServiceMetricsPage = Loadable( () => import( - /* webpackChunkName: "ServiceMetricsPage" */ 'pages/MetricsApplication' + /* webpackChunkName: "ServiceMetricsPage" */ 'pages/MetricsApplication/MetricsApplication' ), ); diff --git a/frontend/src/api/ErrorResponseHandler.ts b/frontend/src/api/ErrorResponseHandler.ts index 027418ec84..be2dd5e31a 100644 --- a/frontend/src/api/ErrorResponseHandler.ts +++ b/frontend/src/api/ErrorResponseHandler.ts @@ -16,7 +16,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse { return { statusCode, payload: null, - error: data.errorType, + error: data.errorType || data.type, message: null, }; } diff --git a/frontend/src/api/IngestionKeys/createIngestionKey.ts b/frontend/src/api/IngestionKeys/createIngestionKey.ts new file mode 100644 index 0000000000..77556ed20a --- /dev/null +++ b/frontend/src/api/IngestionKeys/createIngestionKey.ts @@ -0,0 +1,29 @@ +import { GatewayApiV1Instance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + CreateIngestionKeyProps, + IngestionKeyProps, +} from 'types/api/ingestionKeys/types'; + +const createIngestionKey = async ( + props: CreateIngestionKeyProps, +): Promise | ErrorResponse> => { + try { + const response = await GatewayApiV1Instance.post('/workspaces/me/keys', { + ...props, + }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default createIngestionKey; diff --git a/frontend/src/api/IngestionKeys/deleteIngestionKey.ts b/frontend/src/api/IngestionKeys/deleteIngestionKey.ts new file mode 100644 index 0000000000..5f4e7e02c7 --- /dev/null +++ b/frontend/src/api/IngestionKeys/deleteIngestionKey.ts @@ -0,0 +1,26 @@ +import { GatewayApiV1Instance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { AllIngestionKeyProps } from 'types/api/ingestionKeys/types'; + +const deleteIngestionKey = async ( + id: string, +): Promise | ErrorResponse> => { + try { + const response = await GatewayApiV1Instance.delete( + `/workspaces/me/keys/${id}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default deleteIngestionKey; diff --git a/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts b/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts new file mode 100644 index 0000000000..e202917445 --- /dev/null +++ b/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts @@ -0,0 +1,21 @@ +import { GatewayApiV1Instance } from 'api'; +import { AxiosResponse } from 'axios'; +import { + AllIngestionKeyProps, + GetIngestionKeyProps, +} from 'types/api/ingestionKeys/types'; + +export const getAllIngestionKeys = ( + props: GetIngestionKeyProps, +): Promise> => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { search, per_page, page } = props; + + const BASE_URL = '/workspaces/me/keys'; + const URL_QUERY_PARAMS = + search && search.length > 0 + ? `/search?name=${search}&page=1&per_page=100` + : `?page=${page}&per_page=${per_page}`; + + return GatewayApiV1Instance.get(`${BASE_URL}${URL_QUERY_PARAMS}`); +}; diff --git a/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts b/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts new file mode 100644 index 0000000000..75128b9b78 --- /dev/null +++ b/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-throw-literal */ +import { GatewayApiV1Instance } from 'api'; +import axios from 'axios'; +import { + AddLimitProps, + LimitSuccessProps, +} from 'types/api/ingestionKeys/limits/types'; + +interface SuccessResponse { + statusCode: number; + error: null; + message: string; + payload: T; +} + +interface ErrorResponse { + statusCode: number; + error: string; + message: string; + payload: null; +} + +const createLimitForIngestionKey = async ( + props: AddLimitProps, +): Promise | ErrorResponse> => { + try { + const response = await GatewayApiV1Instance.post( + `/workspaces/me/keys/${props.keyID}/limits`, + { + ...props, + }, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + // Axios error + const errResponse: ErrorResponse = { + statusCode: error.response?.status || 500, + error: error.response?.data?.error, + message: error.response?.data?.status || 'An error occurred', + payload: null, + }; + + throw errResponse; + } else { + // Non-Axios error + const errResponse: ErrorResponse = { + statusCode: 500, + error: 'Unknown error', + message: 'An unknown error occurred', + payload: null, + }; + + throw errResponse; + } + } +}; + +export default createLimitForIngestionKey; diff --git a/frontend/src/api/IngestionKeys/limits/deleteLimitsForIngestionKey.ts b/frontend/src/api/IngestionKeys/limits/deleteLimitsForIngestionKey.ts new file mode 100644 index 0000000000..c0b3480c45 --- /dev/null +++ b/frontend/src/api/IngestionKeys/limits/deleteLimitsForIngestionKey.ts @@ -0,0 +1,26 @@ +import { GatewayApiV1Instance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { AllIngestionKeyProps } from 'types/api/ingestionKeys/types'; + +const deleteLimitsForIngestionKey = async ( + id: string, +): Promise | ErrorResponse> => { + try { + const response = await GatewayApiV1Instance.delete( + `/workspaces/me/limits/${id}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default deleteLimitsForIngestionKey; diff --git a/frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts b/frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts new file mode 100644 index 0000000000..89f3031e08 --- /dev/null +++ b/frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-throw-literal */ +import { GatewayApiV1Instance } from 'api'; +import axios from 'axios'; +import { + LimitSuccessProps, + UpdateLimitProps, +} from 'types/api/ingestionKeys/limits/types'; + +interface SuccessResponse { + statusCode: number; + error: null; + message: string; + payload: T; +} + +interface ErrorResponse { + statusCode: number; + error: string; + message: string; + payload: null; +} + +const updateLimitForIngestionKey = async ( + props: UpdateLimitProps, +): Promise | ErrorResponse> => { + try { + const response = await GatewayApiV1Instance.patch( + `/workspaces/me/limits/${props.limitID}`, + { + config: props.config, + }, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + // Axios error + const errResponse: ErrorResponse = { + statusCode: error.response?.status || 500, + error: error.response?.data?.error, + message: error.response?.data?.status || 'An error occurred', + payload: null, + }; + + throw errResponse; + } else { + // Non-Axios error + const errResponse: ErrorResponse = { + statusCode: 500, + error: 'Unknown error', + message: 'An unknown error occurred', + payload: null, + }; + + throw errResponse; + } + } +}; + +export default updateLimitForIngestionKey; diff --git a/frontend/src/api/IngestionKeys/updateIngestionKey.ts b/frontend/src/api/IngestionKeys/updateIngestionKey.ts new file mode 100644 index 0000000000..c4777ef97f --- /dev/null +++ b/frontend/src/api/IngestionKeys/updateIngestionKey.ts @@ -0,0 +1,32 @@ +import { GatewayApiV1Instance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + IngestionKeysPayloadProps, + UpdateIngestionKeyProps, +} from 'types/api/ingestionKeys/types'; + +const updateIngestionKey = async ( + props: UpdateIngestionKeyProps, +): Promise | ErrorResponse> => { + try { + const response = await GatewayApiV1Instance.patch( + `/workspaces/me/keys/${props.id}`, + { + ...props.data, + }, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default updateIngestionKey; diff --git a/frontend/src/api/apiV1.ts b/frontend/src/api/apiV1.ts index 4fba137e18..05b4e62e78 100644 --- a/frontend/src/api/apiV1.ts +++ b/frontend/src/api/apiV1.ts @@ -3,6 +3,7 @@ const apiV1 = '/api/v1/'; export const apiV2 = '/api/v2/'; export const apiV3 = '/api/v3/'; export const apiV4 = '/api/v4/'; +export const gatewayApiV1 = '/api/gateway/v1'; export const apiAlertManager = '/api/alertmanager'; export default apiV1; diff --git a/frontend/src/api/dashboard/update.ts b/frontend/src/api/dashboard/update.ts index db5350849e..21216e051f 100644 --- a/frontend/src/api/dashboard/update.ts +++ b/frontend/src/api/dashboard/update.ts @@ -1,26 +1,20 @@ import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, Props } from 'types/api/dashboard/update'; const updateDashboard = async ( props: Props, ): Promise | ErrorResponse> => { - try { - const response = await axios.put(`/dashboards/${props.uuid}`, { - ...props.data, - }); + const response = await axios.put(`/dashboards/${props.uuid}`, { + ...props.data, + }); - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; }; export default updateDashboard; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 92a06363a1..1ec4cda601 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -9,7 +9,13 @@ import { ENVIRONMENT } from 'constants/env'; import { LOCALSTORAGE } from 'constants/localStorage'; import store from 'store'; -import apiV1, { apiAlertManager, apiV2, apiV3, apiV4 } from './apiV1'; +import apiV1, { + apiAlertManager, + apiV2, + apiV3, + apiV4, + gatewayApiV1, +} from './apiV1'; import { Logout } from './utils'; const interceptorsResponse = ( @@ -134,6 +140,19 @@ ApiV4Instance.interceptors.response.use( ApiV4Instance.interceptors.request.use(interceptorsRequestResponse); // +// gateway Api V1 +export const GatewayApiV1Instance = axios.create({ + baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`, +}); + +GatewayApiV1Instance.interceptors.response.use( + interceptorsResponse, + interceptorRejected, +); + +GatewayApiV1Instance.interceptors.request.use(interceptorsRequestResponse); +// + AxiosAlertManagerInstance.interceptors.response.use( interceptorsResponse, interceptorRejected, diff --git a/frontend/src/api/plannedDowntime/createDowntimeSchedule.ts b/frontend/src/api/plannedDowntime/createDowntimeSchedule.ts new file mode 100644 index 0000000000..128fb9bf69 --- /dev/null +++ b/frontend/src/api/plannedDowntime/createDowntimeSchedule.ts @@ -0,0 +1,44 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; + +import { Recurrence } from './getAllDowntimeSchedules'; + +export interface DowntimeSchedulePayload { + name: string; + description?: string; + alertIds: string[]; + schedule: { + timezone?: string; + startTime?: string; + endTime?: string; + recurrence?: Recurrence; + }; +} + +export interface PayloadProps { + status: string; + data: string; +} + +const createDowntimeSchedule = async ( + props: DowntimeSchedulePayload, +): Promise | ErrorResponse> => { + try { + const response = await axios.post('/downtime_schedules', { + ...props, + }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default createDowntimeSchedule; diff --git a/frontend/src/api/plannedDowntime/deleteDowntimeSchedule.ts b/frontend/src/api/plannedDowntime/deleteDowntimeSchedule.ts new file mode 100644 index 0000000000..81c3602dea --- /dev/null +++ b/frontend/src/api/plannedDowntime/deleteDowntimeSchedule.ts @@ -0,0 +1,19 @@ +import axios from 'api'; +import { useMutation, UseMutationResult } from 'react-query'; + +export interface DeleteDowntimeScheduleProps { + id?: number; +} + +export interface DeleteSchedulePayloadProps { + status: string; + data: string; +} + +export const useDeleteDowntimeSchedule = ( + props: DeleteDowntimeScheduleProps, +): UseMutationResult => + useMutation({ + mutationKey: [props.id], + mutationFn: () => axios.delete(`/downtime_schedules/${props.id}`), + }); diff --git a/frontend/src/api/plannedDowntime/getAllDowntimeSchedules.ts b/frontend/src/api/plannedDowntime/getAllDowntimeSchedules.ts new file mode 100644 index 0000000000..8e77606a3f --- /dev/null +++ b/frontend/src/api/plannedDowntime/getAllDowntimeSchedules.ts @@ -0,0 +1,50 @@ +import axios from 'api'; +import { AxiosError, AxiosResponse } from 'axios'; +import { Option } from 'container/PlannedDowntime/DropdownWithSubMenu/DropdownWithSubMenu'; +import { useQuery, UseQueryResult } from 'react-query'; + +export type Recurrence = { + startTime?: string | null; + endTime?: string | null; + duration?: number | string | null; + repeatType?: string | Option | null; + repeatOn?: string[] | null; +}; + +type Schedule = { + timezone: string | null; + startTime: string | null; + endTime: string | null; + recurrence: Recurrence | null; +}; + +export interface DowntimeSchedules { + id: number; + name: string | null; + description: string | null; + schedule: Schedule | null; + alertIds: string[] | null; + createdAt: string | null; + createdBy: string | null; + updatedAt: string | null; + updatedBy: string | null; +} +export type PayloadProps = { data: DowntimeSchedules[] }; + +export const getAllDowntimeSchedules = async ( + props?: GetAllDowntimeSchedulesPayloadProps, +): Promise> => + axios.get('/downtime_schedules', { params: props }); + +export interface GetAllDowntimeSchedulesPayloadProps { + active?: boolean; + recurrence?: boolean; +} + +export const useGetAllDowntimeSchedules = ( + props?: GetAllDowntimeSchedulesPayloadProps, +): UseQueryResult, AxiosError> => + useQuery, AxiosError>({ + queryKey: ['getAllDowntimeSchedules', props], + queryFn: () => getAllDowntimeSchedules(props), + }); diff --git a/frontend/src/api/plannedDowntime/updateDowntimeSchedule.ts b/frontend/src/api/plannedDowntime/updateDowntimeSchedule.ts new file mode 100644 index 0000000000..3fc747ae7e --- /dev/null +++ b/frontend/src/api/plannedDowntime/updateDowntimeSchedule.ts @@ -0,0 +1,37 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; + +import { DowntimeSchedulePayload } from './createDowntimeSchedule'; + +export interface DowntimeScheduleUpdatePayload { + data: DowntimeSchedulePayload; + id?: number; +} + +export interface PayloadProps { + status: string; + data: string; +} + +const updateDowntimeSchedule = async ( + props: DowntimeScheduleUpdatePayload, +): Promise | ErrorResponse> => { + try { + const response = await axios.put(`/downtime_schedules/${props.id}`, { + ...props.data, + }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default updateDowntimeSchedule; diff --git a/frontend/src/assets/CustomIcons/ApacheIcon.tsx b/frontend/src/assets/CustomIcons/ApacheIcon.tsx new file mode 100644 index 0000000000..42deba96bd --- /dev/null +++ b/frontend/src/assets/CustomIcons/ApacheIcon.tsx @@ -0,0 +1,176 @@ +export default function ApacheIcon(): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/assets/CustomIcons/DockerIcon.tsx b/frontend/src/assets/CustomIcons/DockerIcon.tsx new file mode 100644 index 0000000000..ef91bb7c9d --- /dev/null +++ b/frontend/src/assets/CustomIcons/DockerIcon.tsx @@ -0,0 +1,28 @@ +export default function DockerIcon(): JSX.Element { + return ( + + + + + + + + + + + ); +} diff --git a/frontend/src/assets/CustomIcons/ElasticSearchIcon.tsx b/frontend/src/assets/CustomIcons/ElasticSearchIcon.tsx new file mode 100644 index 0000000000..d251b7d756 --- /dev/null +++ b/frontend/src/assets/CustomIcons/ElasticSearchIcon.tsx @@ -0,0 +1,36 @@ +export default function ElasticSearchIcon(): JSX.Element { + return ( + + + + + + + + + ); +} diff --git a/frontend/src/assets/CustomIcons/GrafanaIcon.tsx b/frontend/src/assets/CustomIcons/GrafanaIcon.tsx new file mode 100644 index 0000000000..c1949dbb95 --- /dev/null +++ b/frontend/src/assets/CustomIcons/GrafanaIcon.tsx @@ -0,0 +1,18 @@ +function GrafanaIcon(): JSX.Element { + return ( + + + + ); +} + +export default GrafanaIcon; diff --git a/frontend/src/assets/CustomIcons/HerokuIcon.tsx b/frontend/src/assets/CustomIcons/HerokuIcon.tsx new file mode 100644 index 0000000000..3d68fdb6e4 --- /dev/null +++ b/frontend/src/assets/CustomIcons/HerokuIcon.tsx @@ -0,0 +1,27 @@ +function HerokuIcon(): JSX.Element { + return ( + + + + + + + + + + + ); +} + +export default HerokuIcon; diff --git a/frontend/src/assets/CustomIcons/JuiceBoxIcon.tsx b/frontend/src/assets/CustomIcons/JuiceBoxIcon.tsx new file mode 100644 index 0000000000..103a48b493 --- /dev/null +++ b/frontend/src/assets/CustomIcons/JuiceBoxIcon.tsx @@ -0,0 +1,82 @@ +function JuiceBoxIcon(): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +export default JuiceBoxIcon; diff --git a/frontend/src/assets/CustomIcons/KubernetesIcon.tsx b/frontend/src/assets/CustomIcons/KubernetesIcon.tsx new file mode 100644 index 0000000000..280febebbe --- /dev/null +++ b/frontend/src/assets/CustomIcons/KubernetesIcon.tsx @@ -0,0 +1,22 @@ +export default function KubernetesIcon(): JSX.Element { + return ( + + + + + ); +} diff --git a/frontend/src/assets/CustomIcons/MagicBallIcon.tsx b/frontend/src/assets/CustomIcons/MagicBallIcon.tsx new file mode 100644 index 0000000000..e37e4bb540 --- /dev/null +++ b/frontend/src/assets/CustomIcons/MagicBallIcon.tsx @@ -0,0 +1,38 @@ +function MagicBallIcon(): JSX.Element { + return ( + + + + + + + + + ); +} + +export default MagicBallIcon; diff --git a/frontend/src/assets/CustomIcons/MongoDBIcon.tsx b/frontend/src/assets/CustomIcons/MongoDBIcon.tsx new file mode 100644 index 0000000000..d0bd3c4680 --- /dev/null +++ b/frontend/src/assets/CustomIcons/MongoDBIcon.tsx @@ -0,0 +1,68 @@ +export default function MongoDBIcon(): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/assets/CustomIcons/MySQLIcon.tsx b/frontend/src/assets/CustomIcons/MySQLIcon.tsx new file mode 100644 index 0000000000..1cebec32ba --- /dev/null +++ b/frontend/src/assets/CustomIcons/MySQLIcon.tsx @@ -0,0 +1,28 @@ +export default function MySQLIcon(): JSX.Element { + return ( + + + + + + + + + + + ); +} diff --git a/frontend/src/assets/CustomIcons/NginxIcon.tsx b/frontend/src/assets/CustomIcons/NginxIcon.tsx new file mode 100644 index 0000000000..6e93057ab1 --- /dev/null +++ b/frontend/src/assets/CustomIcons/NginxIcon.tsx @@ -0,0 +1,22 @@ +function NginxIcon(): JSX.Element { + return ( + + + + + ); +} + +export default NginxIcon; diff --git a/frontend/src/assets/CustomIcons/PostgreSQLIcon.tsx b/frontend/src/assets/CustomIcons/PostgreSQLIcon.tsx new file mode 100644 index 0000000000..b3c2b1eed2 --- /dev/null +++ b/frontend/src/assets/CustomIcons/PostgreSQLIcon.tsx @@ -0,0 +1,40 @@ +export default function PostgreSQLIcon(): JSX.Element { + return ( + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/assets/CustomIcons/RedisIcon.tsx b/frontend/src/assets/CustomIcons/RedisIcon.tsx new file mode 100644 index 0000000000..714eeb302f --- /dev/null +++ b/frontend/src/assets/CustomIcons/RedisIcon.tsx @@ -0,0 +1,60 @@ +export default function RedisIcon(): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/assets/CustomIcons/TentIcon.tsx b/frontend/src/assets/CustomIcons/TentIcon.tsx new file mode 100644 index 0000000000..9271324fae --- /dev/null +++ b/frontend/src/assets/CustomIcons/TentIcon.tsx @@ -0,0 +1,110 @@ +function TentIcon(): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default TentIcon; diff --git a/frontend/src/components/MarkdownRenderer/CodeCopyBtn/CodeCopyBtn.tsx b/frontend/src/components/MarkdownRenderer/CodeCopyBtn/CodeCopyBtn.tsx index e23882cc71..b098c4228e 100644 --- a/frontend/src/components/MarkdownRenderer/CodeCopyBtn/CodeCopyBtn.tsx +++ b/frontend/src/components/MarkdownRenderer/CodeCopyBtn/CodeCopyBtn.tsx @@ -1,25 +1,33 @@ +/* eslint-disable prefer-destructuring */ import './CodeCopyBtn.scss'; import { CheckOutlined, CopyOutlined } from '@ant-design/icons'; import cx from 'classnames'; -import { useState } from 'react'; +import React, { useState } from 'react'; -export default function CodeCopyBtn({ +function CodeCopyBtn({ children, + onCopyClick, }: { children: React.ReactNode; + onCopyClick?: (additionalInfo?: Record) => void; }): JSX.Element { const [isSnippetCopied, setIsSnippetCopied] = useState(false); const handleClick = (): void => { + let copiedText = ''; if (children && Array.isArray(children)) { setIsSnippetCopied(true); navigator.clipboard.writeText(children[0].props.children[0]).finally(() => { + copiedText = (children[0].props.children[0] as string).slice(0, 200); // slicing is done due to the limitation in accepted char length in attributes setTimeout(() => { setIsSnippetCopied(false); }, 1000); }); + copiedText = (children[0].props.children[0] as string).slice(0, 200); } + + onCopyClick?.({ copiedText }); }; return ( @@ -30,3 +38,9 @@ export default function CodeCopyBtn({ ); } + +CodeCopyBtn.defaultProps = { + onCopyClick: (): void => {}, +}; + +export default CodeCopyBtn; diff --git a/frontend/src/components/MarkdownRenderer/MarkdownRenderer.tsx b/frontend/src/components/MarkdownRenderer/MarkdownRenderer.tsx index 20be0677bd..3b20454ac5 100644 --- a/frontend/src/components/MarkdownRenderer/MarkdownRenderer.tsx +++ b/frontend/src/components/MarkdownRenderer/MarkdownRenderer.tsx @@ -2,6 +2,8 @@ /* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ +import logEvent from 'api/common/logEvent'; +import { isEmpty } from 'lodash-es'; import ReactMarkdown from 'react-markdown'; import { CodeProps } from 'react-markdown/lib/ast-to-react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; @@ -15,10 +17,28 @@ interface LinkProps { children: React.ReactElement; } -function Pre({ children }: { children: React.ReactNode }): JSX.Element { +function Pre({ + children, + elementDetails, + trackCopyAction, +}: { + children: React.ReactNode; + trackCopyAction: boolean; + elementDetails: Record; +}): JSX.Element { + const { trackingTitle = '', ...rest } = elementDetails; + + const handleClick = (additionalInfo?: Record): void => { + const trackingData = { ...rest, copiedContent: additionalInfo }; + + if (trackCopyAction && !isEmpty(trackingTitle)) { + logEvent(trackingTitle as string, trackingData); + } + }; + return (
-			{children}
+			{children}
 			{children}
 		
); @@ -83,9 +103,13 @@ function CustomTag({ color }: { color: string }): JSX.Element { function MarkdownRenderer({ markdownContent, variables, + trackCopyAction, + elementDetails, }: { markdownContent: any; variables: any; + trackCopyAction?: boolean; + elementDetails?: Record; }): JSX.Element { const interpolatedMarkdown = interpolateMarkdown(markdownContent, variables); @@ -96,7 +120,12 @@ function MarkdownRenderer({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore a: Link, - pre: Pre, + pre: ({ children }) => + Pre({ + children, + elementDetails: elementDetails ?? {}, + trackCopyAction: !!trackCopyAction, + }), code: Code, customtag: CustomTag, }} @@ -106,4 +135,9 @@ function MarkdownRenderer({ ); } +MarkdownRenderer.defaultProps = { + elementDetails: {}, + trackCopyAction: false, +}; + export { Code, Link, MarkdownRenderer, Pre }; diff --git a/frontend/src/components/Tags/Tags.styles.scss b/frontend/src/components/Tags/Tags.styles.scss new file mode 100644 index 0000000000..1990b16269 --- /dev/null +++ b/frontend/src/components/Tags/Tags.styles.scss @@ -0,0 +1,38 @@ +.tags-container { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + + .tags { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + } + + .ant-form-item { + margin-bottom: 0; + } + + .ant-tag { + margin-right: 0; + background: var(--bg-vanilla-100); + } +} + +.add-tag-container { + display: flex; + align-items: center; + gap: 4px; + + .ant-form-item { + margin-bottom: 0; + } + + .confirm-cancel-actions { + display: flex; + align-items: center; + gap: 2px; + } +} diff --git a/frontend/src/components/Tags/Tags.tsx b/frontend/src/components/Tags/Tags.tsx new file mode 100644 index 0000000000..ac38e0e58c --- /dev/null +++ b/frontend/src/components/Tags/Tags.tsx @@ -0,0 +1,138 @@ +import './Tags.styles.scss'; + +import { PlusOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import { Tag } from 'antd/lib'; +import Input from 'components/Input'; +import { Check, X } from 'lucide-react'; +import { TweenOneGroup } from 'rc-tween-one'; +import React, { Dispatch, SetStateAction, useState } from 'react'; + +function Tags({ tags, setTags }: AddTagsProps): JSX.Element { + const [inputValue, setInputValue] = useState(''); + const [inputVisible, setInputVisible] = useState(false); + + const handleInputConfirm = (): void => { + if (tags.indexOf(inputValue) > -1) { + return; + } + + if (inputValue) { + setTags([...tags, inputValue]); + } + setInputVisible(false); + setInputValue(''); + }; + + const handleClose = (removedTag: string): void => { + const newTags = tags.filter((tag) => tag !== removedTag); + setTags(newTags); + }; + + const showInput = (): void => { + setInputVisible(true); + setInputValue(''); + }; + + const hideInput = (): void => { + setInputValue(''); + setInputVisible(false); + }; + + const onChangeHandler = ( + value: string, + func: Dispatch>, + ): void => { + func(value); + }; + + const forMap = (tag: string): React.ReactElement => ( + + { + e.preventDefault(); + handleClose(tag); + }} + > + {tag} + + + ); + + const tagChild = tags.map(forMap); + + const renderTagsAnimated = (): React.ReactElement => ( + { + if (e.type === 'appear' || e.type === 'enter') { + (e.target as any).style = 'display: inline-block'; + } + }} + > + {tagChild} + + ); + + return ( +
+ {renderTagsAnimated()} + {inputVisible && ( +
+ + onChangeHandler(event.target.value, setInputValue) + } + onPressEnterHandler={handleInputConfirm} + /> + +
+
+
+ )} + + {!inputVisible && ( + + )} +
+ ); +} + +interface AddTagsProps { + tags: string[]; + setTags: Dispatch>; +} + +export default Tags; diff --git a/frontend/src/components/TimePreferenceDropDown/TimePreference.styles.scss b/frontend/src/components/TimePreferenceDropDown/TimePreference.styles.scss new file mode 100644 index 0000000000..bb1d464057 --- /dev/null +++ b/frontend/src/components/TimePreferenceDropDown/TimePreference.styles.scss @@ -0,0 +1,39 @@ +.time-selection-target { + display: flex; + height: 32px; + padding: 6px 6px 6px 8px; + justify-content: space-between; + align-items: center; + gap: 4px; + align-self: stretch; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + box-shadow: none; + + .button-selected-text { + display: flex; + align-items: center; + gap: 6px; + } + + .selected-value { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; /* 133.333% */ + } +} + +.lightMode { + .time-selection-target { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + + .selected-value { + color: var(--bg-ink-300); + } + } +} diff --git a/frontend/src/components/TimePreferenceDropDown/index.tsx b/frontend/src/components/TimePreferenceDropDown/index.tsx index 572593af7c..d1a6dd275c 100644 --- a/frontend/src/components/TimePreferenceDropDown/index.tsx +++ b/frontend/src/components/TimePreferenceDropDown/index.tsx @@ -1,13 +1,15 @@ +import './TimePreference.styles.scss'; + import { DownOutlined } from '@ant-design/icons'; -import { Button, Dropdown } from 'antd'; +import { Button, Dropdown, Typography } from 'antd'; import TimeItems, { timePreferance, timePreferenceType, } from 'container/NewWidget/RightContainer/timeItems'; +import { Globe } from 'lucide-react'; import { Dispatch, SetStateAction, useCallback, useMemo } from 'react'; import { menuItems } from './config'; -import { TextContainer } from './styles'; function TimePreference({ setSelectedTime, @@ -32,13 +34,22 @@ function TimePreference({ ); return ( - - - - - + + + ); } diff --git a/frontend/src/components/facingIssueBtn/FacingIssueBtn.style.scss b/frontend/src/components/facingIssueBtn/FacingIssueBtn.style.scss index 4cf2290488..68602d3aca 100644 --- a/frontend/src/components/facingIssueBtn/FacingIssueBtn.style.scss +++ b/frontend/src/components/facingIssueBtn/FacingIssueBtn.style.scss @@ -7,3 +7,10 @@ border-color: var(--bg-amber-300) !important; } } + +.tooltip-overlay { + text-wrap: nowrap; + .ant-tooltip-inner { + width: max-content; + } +} diff --git a/frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx b/frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx index d4813acc06..2a4b07aa22 100644 --- a/frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx +++ b/frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx @@ -39,7 +39,12 @@ function FacingIssueBtn({ return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
- + + )} +
+ + + + ); +} diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss index 9efb621385..29d578f096 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss @@ -17,6 +17,10 @@ border-radius: 3px; } + .height-widget { + height: calc(100% - 40px); + } + .list-graph-container { height: calc(100% - 40px); overflow-y: auto; diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts index d8bf328b4d..698d9e6ffa 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts @@ -27,5 +27,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP TRACE: false, BAR: true, PIE: false, + HISTOGRAM: false, EMPTY_WIDGET: false, }; diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx index 84df755964..184c34e77b 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx @@ -42,6 +42,7 @@ function FullView({ fullViewOptions = true, version, originalName, + tableProcessedDataRef, isDependedDataLoaded = false, onToggleModelHandler, }: FullViewProps): JSX.Element { @@ -203,6 +204,7 @@ function FullView({
diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts index 7440278fd8..c4e638d717 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts @@ -2,6 +2,7 @@ import { CheckboxChangeEvent } from 'antd/es/checkbox'; import { ToggleGraphProps } from 'components/Graph/types'; import { UplotProps } from 'components/Uplot/Uplot'; import { PANEL_TYPES } from 'constants/queryBuilder'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin'; import { Dispatch, MutableRefObject, SetStateAction } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; @@ -50,6 +51,7 @@ export interface FullViewProps { fullViewOptions?: boolean; onClickHandler?: OnClickPluginOpts['onClick']; name: string; + tableProcessedDataRef: MutableRefObject; version?: string; originalName: string; yAxisUnit?: string; diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index 8111f386fc..99c32731cc 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -12,6 +12,7 @@ import { useNotifications } from 'hooks/useNotifications'; import useUrlQuery from 'hooks/useUrlQuery'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { Dispatch, @@ -33,7 +34,6 @@ import FullView from './FullView'; import { Modal } from './styles'; import { WidgetGraphComponentProps } from './types'; import { getLocalStorageGraphVisibilityState } from './utils'; -// import { getLocalStorageGraphVisibilityState } from './utils'; function WidgetGraphComponent({ widget, @@ -72,6 +72,8 @@ function WidgetGraphComponent({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const tableProcessedDataRef = useRef([]); + const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard(); const featureResponse = useSelector( @@ -158,7 +160,11 @@ function WidgetGraphComponent({ }, }, { - onSuccess: () => { + onSuccess: (updatedDashboard) => { + if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); + if (setSelectedDashboard && updatedDashboard.payload) { + setSelectedDashboard(updatedDashboard.payload); + } notifications.success({ message: 'Panel cloned successfully, redirecting to new copy.', }); @@ -209,7 +215,7 @@ function WidgetGraphComponent({ const { graphVisibilityStates: localStoredVisibilityState, } = getLocalStorageGraphVisibilityState({ - apiResponse: queryResponse.data.payload.data.result, + apiResponse: queryResponse.data?.payload?.data?.result, name: widget.id, }); setGraphVisibility(localStoredVisibilityState); @@ -284,6 +290,7 @@ function WidgetGraphComponent({ widget={widget} yAxisUnit={widget.yAxisUnit} onToggleModelHandler={onToggleModelHandler} + tableProcessedDataRef={tableProcessedDataRef} /> @@ -301,6 +308,7 @@ function WidgetGraphComponent({ headerMenuList={headerMenuList} isWarning={isWarning} isFetchingResponse={isFetchingResponse} + tableProcessedDataRef={tableProcessedDataRef} /> {queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && ( @@ -319,6 +327,7 @@ function WidgetGraphComponent({ graphVisibility={graphVisibility} onClickHandler={onClickHandler} onDragSelect={onDragSelect} + tableProcessedDataRef={tableProcessedDataRef} /> )} diff --git a/frontend/src/container/GridCardLayout/GridCard/index.tsx b/frontend/src/container/GridCardLayout/GridCard/index.tsx index 43f7768412..a553fe7241 100644 --- a/frontend/src/container/GridCardLayout/GridCard/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/index.tsx @@ -8,6 +8,7 @@ import { useIntersectionObserver } from 'hooks/useIntersectionObserver'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import getTimeString from 'lib/getTimeString'; +import { isEqual } from 'lodash-es'; import isEmpty from 'lodash-es/isEmpty'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { memo, useEffect, useRef, useState } from 'react'; @@ -125,6 +126,16 @@ function GridCardGraph({ }; }); + useEffect(() => { + if (!isEqual(updatedQuery, requestData.query)) { + setRequestData((prev) => ({ + ...prev, + query: updatedQuery, + })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [updatedQuery]); + const queryResponse = useGetQueryRange( { ...requestData, diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss b/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss index 7aef8ee0f0..77e3a0822e 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss +++ b/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss @@ -6,12 +6,85 @@ border: none !important; margin-top: 0; + .row-panel { + border-radius: 4px; + background: rgba(18, 19, 23, 0.4); + padding: 8px; + display: flex; + gap: 6px; + align-items: center; + + .settings-icon { + color: var(--bg-vanilla-400); + cursor: pointer; + } + + .row-icon { + color: var(--bg-vanilla-400); + cursor: pointer; + } + + .grip { + cursor: move; + } + + .section-title { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + .widget-graph-container { &.graph { height: 100%; } } } + + .footer { + display: flex; + flex-direction: column; + position: absolute; + bottom: 0; + width: -webkit-fill-available; + + .locked-text { + align-self: flex-end; + width: 80px; + border: none; + cursor: default; + display: inline-flex; + padding: 4px 6px; + align-items: center; + gap: 4px; + border-radius: 4px 0px 0px 0px; + background: var(--bg-sakura-500); + backdrop-filter: blur(6px); + color: var(--bg-ink-500); + font-family: 'Space Mono'; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; /* 133.333% */ + letter-spacing: 0.48px; + text-transform: uppercase; + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + + .locked-bar { + background: var(--bg-sakura-500); + height: 6px; + width: 100%; + } + } } .widget-graph-container { @@ -32,18 +105,257 @@ } } +.row-settings { + .ant-popover-inner { + width: 191px; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--bg-slate-400); + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ); + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px); + padding: 0px; + + .menu-content { + .section-1 { + .rename-btn { + display: flex; + align-items: center; + gap: 6px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + padding: 14px; + width: 100%; + .ant-btn-icon { + margin-inline-end: 0px; + } + } + } + + .section-2 { + border-top: 1px solid #1d212d; + .remove-section { + display: flex; + align-items: center; + width: 100%; + gap: 6px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + padding: 10px 18px 12px 14px; + color: var(--bg-cherry-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + .ant-btn-icon { + margin-inline-end: 0px; + } + } + } + } + } +} + +.rename-section { + .ant-modal-content { + width: 384px; + height: auto; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--Ink-400, #121317); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0px; + + .ant-modal-header { + padding: 16px; + background: var(--bg-ink-400); + border-bottom: 1px solid var(--bg-slate-500); + margin-bottom: 0px; + + .ant-modal-title { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + } + } + + .ant-modal-body { + padding: 12px 16px 16px 16px; + + .typography { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + } + + .ant-form-item { + margin-bottom: 0px; + + .ant-input { + margin-top: 8px; + margin-bottom: 24px; + } + + .action-btns { + display: flex; + align-items: center; + flex-direction: row-reverse; + gap: 12px; + + .ok-btn { + display: flex; + align-items: center; + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 200% */ + display: flex; + width: 140px; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 2px; + background: var(--bg-robin-500); + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + + .cancel-btn { + display: flex; + align-items: center; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 200% */ + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 2px; + background: var(--bg-slate-500); + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + } + } + } + } +} + .lightMode { .fullscreen-grid-container { - background-color: rgb(250, 250, 250); + .react-grid-layout { + .row-panel { + background: var(--bg-vanilla-200); + + .settings-icon { + color: var(--bg-ink-400); + } + + .row-icon { + color: var(--bg-ink-400); + } + + .section-title { + color: var(--bg-ink-400); + } + } + } } .widget-full-view { .ant-modal-content { background-color: var(--bg-vanilla-100); - } - .ant-modal-header { - background-color: var(--bg-vanilla-100); + .ant-modal-header { + background-color: var(--bg-vanilla-100); + } + } + } + + .row-settings { + .ant-popover-inner { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .menu-content { + .section-1 { + .rename-btn { + color: var(--bg-ink-400); + } + } + + .section-2 { + border-top: 1px solid var(--bg-vanilla-300); + .remove-section { + color: var(--bg-cherry-400); + } + } + } + } + } + + .rename-section { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .ant-modal-header { + background: var(--bg-vanilla-100); + border-bottom: 1px solid var(--bg-vanilla-300); + + .ant-modal-title { + color: var(--bg-ink-300); + } + } + + .ant-modal-body { + .typography { + color: var(--bg-ink-100); + } + + .ant-form-item { + .action-btns { + .cancel-btn { + color: var(--bg-ink-300); + background: var(--bg-vanilla-300); + } + } + } + } } } } diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.tsx b/frontend/src/container/GridCardLayout/GridCardLayout.tsx index 867410744c..11bdf492ee 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.tsx +++ b/frontend/src/container/GridCardLayout/GridCardLayout.tsx @@ -1,15 +1,14 @@ import './GridCardLayout.styles.scss'; -import { PlusOutlined } from '@ant-design/icons'; -import { Flex, Form, Input, Modal, Tooltip, Typography } from 'antd'; +import { Color } from '@signozhq/design-tokens'; +import { Button, Form, Input, Modal, Typography } from 'antd'; import { useForm } from 'antd/es/form/Form'; import cx from 'classnames'; -import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; -import { dashboardHelpMessage } from 'components/facingIssueBtn/util'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; import { themeColors } from 'constants/theme'; +import { DEFAULT_ROW_NAME } from 'container/NewDashboard/DashboardDescription/utils'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import useComponentPermission from 'hooks/useComponentPermission'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -19,19 +18,18 @@ import history from 'lib/history'; import { defaultTo } from 'lodash-es'; import isEqual from 'lodash-es/isEqual'; import { - FullscreenIcon, + Check, + ChevronDown, + ChevronUp, GripVertical, - MoveDown, - MoveUp, - Settings, - Trash2, + LockKeyhole, + X, } from 'lucide-react'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { sortLayout } from 'providers/Dashboard/util'; -import { useCallback, useEffect, useState } from 'react'; -import { FullScreen, useFullScreenHandle } from 'react-full-screen'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { FullScreen, FullScreenHandle } from 'react-full-screen'; import { ItemCallback, Layout } from 'react-grid-layout'; -import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { UpdateTimeInterval } from 'store/actions'; @@ -40,21 +38,21 @@ import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; import { ROLES, USER_ROLES } from 'types/roles'; import { ComponentTypes } from 'utils/permission'; -import { v4 as uuid } from 'uuid'; import { EditMenuAction, ViewMenuAction } from './config'; +import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState'; import GridCard from './GridCard'; -import { - Button, - ButtonContainer, - Card, - CardContainer, - ReactGridLayout, -} from './styles'; -import { GraphLayoutProps } from './types'; +import { Card, CardContainer, ReactGridLayout } from './styles'; import { removeUndefinedValuesFromLayout } from './utils'; +import { WidgetRowHeader } from './WidgetRow'; -function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { +interface GraphLayoutProps { + handle: FullScreenHandle; +} + +// eslint-disable-next-line sonarjs/cognitive-complexity +function GraphLayout(props: GraphLayoutProps): JSX.Element { + const { handle } = props; const { selectedDashboard, layouts, @@ -65,14 +63,11 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { isDashboardLocked, } = useDashboard(); const { data } = selectedDashboard || {}; - const handle = useFullScreenHandle(); const { pathname } = useLocation(); const dispatch = useDispatch(); const { widgets, variables } = data || {}; - const { t } = useTranslation(['dashboard']); - const { featureResponse, role, user } = useSelector( (state) => state.app, ); @@ -122,6 +117,11 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { userRole, ); + const [deleteWidget, editWidget] = useComponentPermission( + ['delete_widget', 'edit_widget'], + role, + ); + useEffect(() => { setDashboardLayout(sortLayout(layouts)); }, [layouts]); @@ -206,80 +206,6 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dashboardLayout]); - function handleAddRow(): void { - if (!selectedDashboard) return; - const id = uuid(); - - const newRowWidgetMap: { widgets: Layout[]; collapsed: boolean } = { - widgets: [], - collapsed: false, - }; - const currentRowIdx = 0; - for (let j = currentRowIdx; j < dashboardLayout.length; j++) { - if (!currentPanelMap[dashboardLayout[j].i]) { - newRowWidgetMap.widgets.push(dashboardLayout[j]); - } else { - break; - } - } - - const updatedDashboard: Dashboard = { - ...selectedDashboard, - data: { - ...selectedDashboard.data, - layout: [ - { - i: id, - w: 12, - minW: 12, - minH: 1, - maxH: 1, - x: 0, - h: 1, - y: 0, - }, - ...dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET), - ], - panelMap: { ...currentPanelMap, [id]: newRowWidgetMap }, - widgets: [ - ...(selectedDashboard.data.widgets || []), - { - id, - title: 'Sample Row', - description: '', - panelTypes: PANEL_GROUP_TYPES.ROW, - }, - ], - }, - uuid: selectedDashboard.uuid, - }; - - updateDashboardMutation.mutate(updatedDashboard, { - // eslint-disable-next-line sonarjs/no-identical-functions - onSuccess: (updatedDashboard) => { - if (updatedDashboard.payload) { - if (updatedDashboard.payload.data.layout) - setLayouts(sortLayout(updatedDashboard.payload.data.layout)); - setSelectedDashboard(updatedDashboard.payload); - setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); - } - - featureResponse.refetch(); - }, - // eslint-disable-next-line sonarjs/no-identical-functions - onError: () => { - notifications.error({ - message: SOMETHING_WENT_WRONG, - }); - }, - }); - } - - const handleRowSettingsClick = (id: string): void => { - setIsSettingsModalOpen(true); - setCurrentSelectRowId(id); - }; - const onSettingsModalSubmit = (): void => { const newTitle = form.getFieldValue('title'); if (!selectedDashboard) return; @@ -330,6 +256,15 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { }); }; + useEffect(() => { + if (!currentSelectRowId) return; + form.setFieldValue( + 'title', + (widgets?.find((widget) => widget.id === currentSelectRowId) + ?.title as string) || DEFAULT_ROW_NAME, + ); + }, [currentSelectRowId, form, widgets]); + // eslint-disable-next-line sonarjs/cognitive-complexity const handleRowCollapse = (id: string): void => { if (!selectedDashboard) return; @@ -483,192 +418,187 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element { }, }); }; - return ( - <> - - - - - - )} - {!isDashboardLocked && addPanelPermission && ( - - )} - - - - - - {dashboardLayout.map((layout) => { - const { i: id } = layout; - const currentWidget = (widgets || [])?.find((e) => e.id === id); - - if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) { - const rowWidgetProperties = currentPanelMap[id] || {}; - return ( - -
-
-
- {rowWidgetProperties.collapsed && ( -
-
- ); - } + const isDashboardEmpty = useMemo( + () => + selectedDashboard?.data.layout + ? selectedDashboard?.data.layout?.length === 0 + : true, + [selectedDashboard], + ); + return isDashboardEmpty ? ( + + ) : ( + + + {dashboardLayout.map((layout) => { + const { i: id } = layout; + const currentWidget = (widgets || [])?.find((e) => e.id === id); + if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) { + const rowWidgetProperties = currentPanelMap[id] || {}; return ( - - +
+ {rowWidgetProperties.collapsed && ( + + )} + + {currentWidget.title} + + {rowWidgetProperties.collapsed ? ( + handleRowCollapse(id)} + className="row-icon" + /> + ) : ( + handleRowCollapse(id)} + className="row-icon" + /> + )} +
+ -
+
); - })} -
- { - setIsSettingsModalOpen(false); - setCurrentSelectRowId(null); - }} - > -
- - widget.id === currentSelectRowId) - ?.title as string, - 'Sample Title', - )} - /> - - - +
+
+ )} + { + setIsSettingsModalOpen(false); + setCurrentSelectRowId(null); + }} + > + + + Enter section name + + + widget.id === currentSelectRowId) + ?.title as string, + 'Sample Title', + )} + /> + + +
+ - - - - { - setIsDeleteModalOpen(false); - setCurrentSelectRowId(null); - }} - onOk={(): void => handleRowDelete()} - > - Are you sure you want to delete this row - - - + +
+
+ +
+ { + setIsDeleteModalOpen(false); + setCurrentSelectRowId(null); + }} + onOk={(): void => handleRowDelete()} + > + Are you sure you want to delete this row + +
); } diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/contants.ts b/frontend/src/container/GridCardLayout/WidgetHeader/contants.ts index 6f3871bd3d..7c32a0e2c5 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/contants.ts +++ b/frontend/src/container/GridCardLayout/WidgetHeader/contants.ts @@ -4,6 +4,7 @@ export enum MenuItemKeys { Delete = 'delete', Clone = 'clone', CreateAlerts = 'createAlerts', + Download = 'download', } export const MENUITEM_KEYS_VS_LABELS = { @@ -12,4 +13,5 @@ export const MENUITEM_KEYS_VS_LABELS = { [MenuItemKeys.Delete]: 'Delete', [MenuItemKeys.Clone]: 'Clone', [MenuItemKeys.CreateAlerts]: 'Create Alerts', + [MenuItemKeys.Download]: 'Download as CSV', }; diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx index 63312a5225..1401576ca9 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx @@ -2,6 +2,7 @@ import './WidgetHeader.styles.scss'; import { AlertOutlined, + CloudDownloadOutlined, CopyOutlined, DeleteOutlined, EditFilled, @@ -17,6 +18,9 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { isEmpty } from 'lodash-es'; +import { unparse } from 'papaparse'; import { ReactNode, useCallback, useMemo } from 'react'; import { UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; @@ -46,6 +50,7 @@ interface IWidgetHeaderProps { headerMenuList?: MenuItemKeys[]; isWarning: boolean; isFetchingResponse: boolean; + tableProcessedDataRef: React.MutableRefObject; } function WidgetHeader({ @@ -61,6 +66,7 @@ function WidgetHeader({ headerMenuList, isWarning, isFetchingResponse, + tableProcessedDataRef, }: IWidgetHeaderProps): JSX.Element | null { const onEditHandler = useCallback((): void => { const widgetId = widget.id; @@ -75,6 +81,17 @@ function WidgetHeader({ const onCreateAlertsHandler = useCreateAlerts(widget); + const onDownloadHandler = useCallback((): void => { + const csv = unparse(tableProcessedDataRef.current); + const csvBlob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const csvUrl = URL.createObjectURL(csvBlob); + const downloadLink = document.createElement('a'); + downloadLink.href = csvUrl; + downloadLink.download = `${!isEmpty(title) ? title : 'table-panel'}.csv`; + downloadLink.click(); + downloadLink.remove(); + }, [tableProcessedDataRef, title]); + const keyMethodMapping = useMemo( () => ({ [MenuItemKeys.View]: onView, @@ -82,8 +99,16 @@ function WidgetHeader({ [MenuItemKeys.Delete]: onDelete, [MenuItemKeys.Clone]: onClone, [MenuItemKeys.CreateAlerts]: onCreateAlertsHandler, + [MenuItemKeys.Download]: onDownloadHandler, }), - [onDelete, onEditHandler, onView, onClone, onCreateAlertsHandler], + [ + onView, + onEditHandler, + onDelete, + onClone, + onCreateAlertsHandler, + onDownloadHandler, + ], ); const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback( @@ -128,6 +153,13 @@ function WidgetHeader({ isVisible: headerMenuList?.includes(MenuItemKeys.Clone) || false, disabled: !editWidget, }, + { + key: MenuItemKeys.Download, + icon: , + label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Download], + isVisible: widget.panelTypes === PANEL_TYPES.TABLE, + disabled: false, + }, { key: MenuItemKeys.Delete, icon: , @@ -144,7 +176,13 @@ function WidgetHeader({ disabled: false, }, ], - [headerMenuList, queryResponse.isFetching, editWidget, deleteWidget], + [ + headerMenuList, + queryResponse.isFetching, + editWidget, + deleteWidget, + widget.panelTypes, + ], ); const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]); diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/utils.ts b/frontend/src/container/GridCardLayout/WidgetHeader/utils.ts index 482994aa8c..96f7e32010 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/utils.ts +++ b/frontend/src/container/GridCardLayout/WidgetHeader/utils.ts @@ -19,4 +19,5 @@ export const isTWidgetOptions = (value: string): value is MenuItemKeys => value === MenuItemKeys.Edit || value === MenuItemKeys.Delete || value === MenuItemKeys.Clone || - value === MenuItemKeys.CreateAlerts; + value === MenuItemKeys.CreateAlerts || + value === MenuItemKeys.Download; diff --git a/frontend/src/container/GridCardLayout/WidgetRow.tsx b/frontend/src/container/GridCardLayout/WidgetRow.tsx new file mode 100644 index 0000000000..70e5b64670 --- /dev/null +++ b/frontend/src/container/GridCardLayout/WidgetRow.tsx @@ -0,0 +1,82 @@ +import { Button, Popover } from 'antd'; +import { EllipsisIcon, PenLine, X } from 'lucide-react'; +import { useState } from 'react'; +import { Layout } from 'react-grid-layout'; + +interface WidgetRowHeaderProps { + rowWidgetProperties: { + widgets: Layout[]; + collapsed: boolean; + }; + editWidget: boolean; + deleteWidget: boolean; + setIsSettingsModalOpen: (value: React.SetStateAction) => void; + setCurrentSelectRowId: (value: React.SetStateAction) => void; + setIsDeleteModalOpen: (value: React.SetStateAction) => void; + id: string; +} + +export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element { + const { + rowWidgetProperties, + editWidget, + deleteWidget, + setCurrentSelectRowId, + setIsDeleteModalOpen, + setIsSettingsModalOpen, + id, + } = props; + const [isRowSettingsOpen, setIsRowSettingsOpen] = useState(false); + return ( + setIsRowSettingsOpen(visible)} + rootClassName="row-settings" + trigger="hover" + placement="bottomRight" + content={ +
+
+ +
+ {!rowWidgetProperties.collapsed && ( +
+ +
+ )} +
+ } + > + setIsRowSettingsOpen(!isRowSettingsOpen)} + /> +
+ ); +} diff --git a/frontend/src/container/GridCardLayout/index.tsx b/frontend/src/container/GridCardLayout/index.tsx index a2a82dc8ad..9ef053491f 100644 --- a/frontend/src/container/GridCardLayout/index.tsx +++ b/frontend/src/container/GridCardLayout/index.tsx @@ -1,16 +1,13 @@ -import { useDashboard } from 'providers/Dashboard/Dashboard'; -import { useCallback } from 'react'; +import { FullScreenHandle } from 'react-full-screen'; import GraphLayoutContainer from './GridCardLayout'; -function GridGraph(): JSX.Element { - const { handleToggleDashboardSlider } = useDashboard(); - - const onEmptyWidgetHandler = useCallback(() => { - handleToggleDashboardSlider(true); - }, [handleToggleDashboardSlider]); - - return ; +interface GridGraphProps { + handle: FullScreenHandle; +} +function GridGraph(props: GridGraphProps): JSX.Element { + const { handle } = props; + return ; } export default GridGraph; diff --git a/frontend/src/container/GridCardLayout/styles.ts b/frontend/src/container/GridCardLayout/styles.ts index ee4144b9c5..e3f24308de 100644 --- a/frontend/src/container/GridCardLayout/styles.ts +++ b/frontend/src/container/GridCardLayout/styles.ts @@ -8,12 +8,28 @@ const ReactGridLayoutComponent = WidthProvider(RGL); interface CardProps { $panelType: PANEL_TYPES; + isDarkMode: boolean; } export const Card = styled(CardComponent)` &&& { height: 100%; overflow: hidden; + border-radius: 3px; + border: 1px solid var(--bg-slate-500); + background: linear-gradient( + 0deg, + rgba(171, 189, 255, 0) 0%, + rgba(171, 189, 255, 0) 100% + ), + #0b0c0e; + + ${({ isDarkMode }): StyledCSS => + !isDarkMode && + css` + border: 1px solid var(--bg-vanilla-300); + background: unset; + `} } .ant-card-body { @@ -34,7 +50,8 @@ export const CardContainer = styled.div` height: 100%; display: flex; justify-content: space-between; - background: var(--bg-ink-400); + background: ${({ isDarkMode }): string => + isDarkMode ? 'var(--bg-ink-400)' : 'var(--bg-vanilla-300)'}; align-items: center; overflow: hidden; } @@ -74,6 +91,7 @@ export const ReactGridLayout = styled(ReactGridLayoutComponent)` margin-top: 1rem; position: relative; min-height: 40vh; + margin: 16px; .react-grid-item.react-grid-placeholder { background: grey; diff --git a/frontend/src/container/GridCardLayout/types.ts b/frontend/src/container/GridCardLayout/types.ts deleted file mode 100644 index a0e8e9aa13..0000000000 --- a/frontend/src/container/GridCardLayout/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GraphLayoutProps { - onAddPanelHandler: VoidFunction; -} diff --git a/frontend/src/container/GridPanelSwitch/index.tsx b/frontend/src/container/GridPanelSwitch/index.tsx index 5a3c55df56..641f06e885 100644 --- a/frontend/src/container/GridPanelSwitch/index.tsx +++ b/frontend/src/container/GridPanelSwitch/index.tsx @@ -49,6 +49,7 @@ const GridPanelSwitch = forwardRef< options, ref, }, + [PANEL_TYPES.HISTOGRAM]: null, [PANEL_TYPES.EMPTY_WIDGET]: null, }; diff --git a/frontend/src/container/GridPanelSwitch/types.ts b/frontend/src/container/GridPanelSwitch/types.ts index 5bc95c2e35..f57d9c7482 100644 --- a/frontend/src/container/GridPanelSwitch/types.ts +++ b/frontend/src/container/GridPanelSwitch/types.ts @@ -43,5 +43,6 @@ export type PropsTypePropsMap = { [PANEL_TYPES.BAR]: UplotProps & { ref: ForwardedRef; }; + [PANEL_TYPES.HISTOGRAM]: null; [PANEL_TYPES.EMPTY_WIDGET]: null; }; diff --git a/frontend/src/container/GridTableComponent/index.tsx b/frontend/src/container/GridTableComponent/index.tsx index db89133553..26bdda1a49 100644 --- a/frontend/src/container/GridTableComponent/index.tsx +++ b/frontend/src/container/GridTableComponent/index.tsx @@ -1,9 +1,14 @@ import { ExclamationCircleFilled } from '@ant-design/icons'; import { Space, Tooltip } from 'antd'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import { Events } from 'constants/events'; import { QueryTable } from 'container/QueryTable'; -import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery'; -import { memo, ReactNode, useEffect, useMemo } from 'react'; +import { + createTableColumnsFromQuery, + RowData, +} from 'lib/query/createTableColumnsFromQuery'; +import { cloneDeep, get, isEmpty, set } from 'lodash-es'; +import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { eventEmitter } from 'utils/getEventEmitter'; @@ -15,10 +20,12 @@ function GridTableComponent({ data, query, thresholds, + columnUnits, + tableProcessedDataRef, ...props }: GridTableComponentProps): JSX.Element { const { t } = useTranslation(['valueGraph']); - const { columns, dataSource } = useMemo( + const { columns, dataSource: originalDataSource } = useMemo( () => createTableColumnsFromQuery({ query, @@ -26,6 +33,61 @@ function GridTableComponent({ }), [data, query], ); + const createDataInCorrectFormat = useCallback( + (dataSource: RowData[]): RowData[] => + dataSource.map((d) => { + const finalObject = {}; + const keys = Object.keys(d); + keys.forEach((k) => { + const label = get( + columns.find((c) => get(c, 'dataIndex', '') === k) || {}, + 'title', + '', + ); + if (label) { + set(finalObject, label as string, d[k]); + } + }); + return finalObject as RowData; + }), + [columns], + ); + + const applyColumnUnits = useCallback( + (dataSource: RowData[]): RowData[] => { + let mutateDataSource = cloneDeep(dataSource); + if (isEmpty(columnUnits)) { + return mutateDataSource; + } + + mutateDataSource = mutateDataSource.map( + (val): RowData => { + const newValue = val; + Object.keys(val).forEach((k) => { + if (columnUnits[k]) { + newValue[k] = getYAxisFormattedValue(String(val[k]), columnUnits[k]); + } + }); + return newValue; + }, + ); + + return mutateDataSource; + }, + [columnUnits], + ); + + const dataSource = useMemo(() => applyColumnUnits(originalDataSource), [ + applyColumnUnits, + originalDataSource, + ]); + + useEffect(() => { + if (tableProcessedDataRef) { + // eslint-disable-next-line no-param-reassign + tableProcessedDataRef.current = createDataInCorrectFormat(dataSource); + } + }, [createDataInCorrectFormat, dataSource, tableProcessedDataRef]); const newColumnData = columns.map((e) => ({ ...e, diff --git a/frontend/src/container/GridTableComponent/types.ts b/frontend/src/container/GridTableComponent/types.ts index 4f210eca18..25ca647933 100644 --- a/frontend/src/container/GridTableComponent/types.ts +++ b/frontend/src/container/GridTableComponent/types.ts @@ -5,11 +5,14 @@ import { ThresholdProps, } from 'container/NewWidget/RightContainer/Threshold/types'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { ColumnUnit } from 'types/api/dashboard/getAll'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; export type GridTableComponentProps = { query: Query; thresholds?: ThresholdProps[]; + columnUnits?: ColumnUnit; + tableProcessedDataRef?: React.MutableRefObject; } & Pick & Omit, 'columns' | 'dataSource'>; diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss index 3d5f41ab33..2c77750cd4 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss +++ b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss @@ -1,3 +1,942 @@ .ingestion-settings-container { color: white; } + +.ingestion-key-container { + margin-top: 24px; + display: flex; + justify-content: center; + width: 100%; + + .ingestion-key-content { + width: calc(100% - 30px); + max-width: 736px; + + .title { + color: var(--bg-vanilla-100); + font-size: var(--font-size-lg); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 28px; + /* 155.556% */ + letter-spacing: -0.09px; + } + + .subtitle { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; + /* 142.857% */ + letter-spacing: -0.07px; + } + + .ingestion-keys-search-add-new { + display: flex; + align-items: center; + gap: 12px; + + padding: 16px 0; + + .add-new-ingestion-key-btn { + display: flex; + align-items: center; + gap: 8px; + } + } + + .ant-table-row { + .ant-table-cell { + padding: 0; + border: none; + background: var(--bg-ink-500); + } + + .column-render { + margin: 8px 0 !important; + border-radius: 6px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + + .title-with-action { + display: flex; + justify-content: space-between; + + align-items: center; + padding: 8px; + + .ingestion-key-data { + display: flex; + gap: 8px; + align-items: center; + + .ingestion-key-title { + display: flex; + align-items: center; + gap: 6px; + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + letter-spacing: -0.07px; + } + } + + .ingestion-key-value { + display: flex; + align-items: center; + gap: 12px; + + border-radius: 20px; + padding: 0px 12px; + + background: var(--bg-ink-200); + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-xs); + font-family: 'Space Mono', monospace; + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + letter-spacing: -0.07px; + } + + .copy-key-btn { + cursor: pointer; + } + } + } + + .action-btn { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + } + + .visibility-btn { + border: 1px solid rgba(113, 144, 249, 0.2); + background: rgba(113, 144, 249, 0.1); + } + } + + .ant-collapse { + border: none; + + .ant-collapse-header { + padding: 0px 8px; + + display: flex; + align-items: center; + background-color: #121317; + } + + .ant-collapse-content { + border-top: 1px solid var(--bg-slate-500); + } + + .ant-collapse-item { + border-bottom: none; + } + + .ant-collapse-expand-icon { + padding-inline-end: 0px; + } + } + + .ingestion-key-details { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + border-top: 1px solid var(--bg-slate-500); + padding: 8px; + + .ingestion-key-tag { + width: 14px; + height: 14px; + border-radius: 50px; + background: var(--bg-slate-300); + display: flex; + justify-content: center; + align-items: center; + + .tag-text { + color: var(--bg-vanilla-400); + leading-trim: both; + text-edge: cap; + font-size: 10px; + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: normal; + letter-spacing: -0.05px; + } + } + + .ingestion-key-created-by { + margin-left: 8px; + } + + .ingestion-key-last-used-at { + display: flex; + align-items: center; + gap: 8px; + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 18px; + /* 128.571% */ + letter-spacing: -0.07px; + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; + } + } + + .ingestion-key-expires-in { + font-style: normal; + font-weight: 400; + line-height: 18px; + + display: flex; + align-items: center; + gap: 8px; + + .dot { + height: 6px; + width: 6px; + border-radius: 50%; + } + + &.warning { + color: var(--bg-amber-400); + + .dot { + background: var(--bg-amber-400); + box-shadow: 0px 0px 6px 0px var(--bg-amber-400); + } + } + + &.danger { + color: var(--bg-cherry-400); + + .dot { + background: var(--bg-cherry-400); + box-shadow: 0px 0px 6px 0px var(--bg-cherry-400); + } + } + } + } + } + } + + .ant-pagination-item { + display: flex; + justify-content: center; + align-items: center; + + > a { + color: var(--bg-vanilla-400); + font-variant-numeric: lining-nums tabular-nums slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'case' on, 'cpsp' on; + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; + /* 142.857% */ + } + } + + .ant-pagination-item-active { + background-color: var(--bg-robin-500); + + > a { + color: var(--bg-ink-500) !important; + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + } + } + } +} + +.ingestion-key-info-container { + display: flex; + gap: 12px; + flex-direction: column; + + .user-info { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + + .user-avatar { + background-color: lightslategray; + vertical-align: middle; + } + } + + .user-email { + display: inline-flex; + align-items: center; + gap: 12px; + border-radius: 20px; + padding: 0px 12px; + background: var(--bg-ink-200); + + font-family: 'Space Mono', monospace; + } + + .role { + display: flex; + align-items: center; + gap: 12px; + } + + .ingestion-key-tags-container { + display: flex; + align-items: center; + gap: 16px; + } + + .limits-data { + padding: 16px; + border: 1px solid var(--bg-slate-500); + + .signals { + .signal { + margin-bottom: 24px; + + .header { + display: flex; + justify-content: space-between; + align-items: center; + + .actions { + display: flex; + align-items: center; + gap: 4px; + } + } + + .signal-name { + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + + color: var(--bg-robin-500); + } + + .signal-limit-values { + display: flex; + gap: 16px; + margin-top: 8px; + margin-bottom: 16px; + + .edit-ingestion-key-limit-form { + width: 100%; + } + + .ant-form-item { + margin-bottom: 12px; + } + + .daily-limit, + .second-limit { + flex: 1; + + .heading { + .title { + font-size: 12px; + } + + .subtitle { + font-size: 11px; + } + + padding: 4px 0px; + } + + .ant-input-number { + width: 80%; + } + } + + .signal-limit-view-mode { + display: flex; + width: 100%; + justify-content: space-between; + gap: 16px; + + .signal-limit-value { + display: flex; + align-items: center; + gap: 8px; + + flex: 1; + + .limit-type { + display: flex; + align-items: center; + gap: 8px; + } + + .limit-value { + display: flex; + align-items: center; + gap: 8px; + + font-size: 12px; + font-weight: 600; + } + } + } + + .signal-limit-edit-mode { + display: flex; + justify-content: space-between; + gap: 16px; + } + } + + .signal-limit-save-discard { + display: flex; + gap: 8px; + } + } + } + } +} + +.ingestion-key-modal { + .ant-modal-content { + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0; + + .ant-modal-header { + background: none; + border-bottom: 1px solid var(--bg-slate-500); + padding: 16px; + } + + .ant-modal-close-x { + font-size: 12px; + } + + .ant-modal-body { + padding: 12px 16px; + } + + .ant-modal-footer { + padding: 16px; + margin-top: 0; + + display: flex; + justify-content: flex-end; + } + } +} + +.ingestion-key-access-role { + display: flex; + + .ant-radio-button-wrapper { + font-size: 12px; + text-transform: capitalize; + + &.ant-radio-button-wrapper-checked { + color: #fff; + background: var(--bg-slate-400, #1d212d); + border-color: var(--bg-slate-400, #1d212d); + + &:hover { + color: #fff; + background: var(--bg-slate-400, #1d212d); + border-color: var(--bg-slate-400, #1d212d); + + &::before { + background-color: var(--bg-slate-400, #1d212d); + } + } + + &:focus { + color: #fff; + background: var(--bg-slate-400, #1d212d); + border-color: var(--bg-slate-400, #1d212d); + } + } + } + + .tab { + border: 1px solid var(--bg-slate-400); + + flex: 1; + + display: flex; + justify-content: center; + + &::before { + background: var(--bg-slate-400); + } + + &.selected { + background: var(--bg-slate-400, #1d212d); + } + } + + .role { + display: flex; + align-items: center; + gap: 8px; + } +} + +.delete-ingestion-key-modal { + width: calc(100% - 30px) !important; + /* Adjust the 20px as needed */ + max-width: 384px; + + .ant-modal-content { + padding: 0; + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + + .ant-modal-header { + padding: 16px; + background: var(--bg-ink-400); + } + + .ant-modal-body { + padding: 0px 16px 28px 16px; + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; + letter-spacing: -0.07px; + } + + .ingestion-key-input { + margin-top: 8px; + display: flex; + gap: 8px; + } + + .ant-color-picker-trigger { + padding: 6px; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + width: 32px; + height: 32px; + + .ant-color-picker-color-block { + border-radius: 50px; + width: 16px; + height: 16px; + flex-shrink: 0; + + .ant-color-picker-color-block-inner { + display: flex; + justify-content: center; + align-items: center; + } + } + } + } + + .ant-modal-footer { + display: flex; + justify-content: flex-end; + padding: 16px 16px; + margin: 0; + + .cancel-btn { + display: flex; + align-items: center; + border: none; + border-radius: 2px; + background: var(--bg-slate-500); + } + + .delete-btn { + display: flex; + align-items: center; + border: none; + border-radius: 2px; + background: var(--bg-cherry-500); + margin-left: 12px; + } + + .delete-btn:hover { + color: var(--bg-vanilla-100); + background: var(--bg-cherry-600); + } + } + } + + .title { + color: var(--bg-vanilla-100); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + /* 142.857% */ + } +} + +.expires-at { + .ant-picker { + border-color: var(--bg-slate-400) !important; + } +} + +.expiration-selector { + .ant-select-selector { + border: 1px solid var(--bg-slate-400) !important; + } +} + +.newAPIKeyDetails { + display: flex; + flex-direction: column; + gap: 8px; +} + +.copyable-text { + display: inline-flex; + align-items: center; + gap: 12px; + border-radius: 20px; + padding: 0px 12px; + background: var(--bg-ink-200, #23262e); + + .copy-key-btn { + cursor: pointer; + } +} + +.ingestion-key-details-edit-drawer-title { + display: flex; + gap: 8px; +} + +.ingestion-key-details-meta { + padding: 14px 16px; + border-radius: 3px; + border: 1px solid var(--Slate-500, #161922); +} + +#edit-ingestion-key-form { + .ant-form-item:last-child { + margin-bottom: 0px; + } +} + +.alert { + display: flex; + gap: 12px; + + padding: 8px; + margin: 16px 0; + + border-radius: 4px; + background: rgba(113, 144, 249, 0.1); + color: var(--Robin-300, #95acfb); + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 160%; + letter-spacing: 0.013px; +} + +.error { + color: var(--bg-cherry-500); + margin-bottom: 8px; +} + +.save-discard-changes { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.ingestion-details-edit-drawer { + .ant-drawer-header { + border-bottom: 1px solid var(--Slate-500, #161922); + padding: 8px; + } + + .ant-drawer-footer { + border-top: 1px solid var(--Slate-500, #161922); + padding: 8px; + } +} + +.ingestion-key-limits { + margin-top: 48px; + padding: 16px; + border-radius: 3px; + border: 1px solid var(--Slate-500, #161922); + + .ant-tabs { + .ant-tabs-nav { + margin-top: -36px; + + &::before { + border-bottom: none; + } + + .ant-tabs-nav-list { + background: #121317; + border-radius: 2px; + border: 1px solid var(--Slate-400, #1d212d); + background: var(--Ink-400, #121317); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + + .ant-tabs-tab { + display: inline-flex; + padding: 6px 36px; + justify-content: center; + align-items: center; + margin: 0px; + border-right: 1px solid #1d212d; + + &.ant-tabs-tab-active { + border-bottom: 0px; + } + + .tab-name { + display: flex; + align-items: center; + gap: 8px; + } + } + } + } + } +} + +.ingestion-key-expires-at { + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); +} + +.lightMode { + .ingestion-key-container { + .ingestion-key-content { + .title { + color: var(--bg-ink-500); + } + + .ant-table-row { + .ant-table-cell { + background: var(--bg-vanilla-200); + } + + &:hover { + .ant-table-cell { + background: var(--bg-vanilla-200) !important; + } + } + + .column-render { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + + .ant-collapse { + border: none; + + .ant-collapse-header { + background: var(--bg-vanilla-100); + } + + .ant-collapse-content { + border-top: 1px solid var(--bg-vanilla-300); + } + } + + .title-with-action { + .ingestion-key-title { + .ant-typography { + color: var(--bg-ink-500); + } + } + + .ingestion-key-value { + background: var(--bg-vanilla-200); + + .ant-typography { + color: var(--bg-slate-400); + } + + .copy-key-btn { + cursor: pointer; + } + } + + .action-btn { + .ant-typography { + color: var(--bg-ink-500); + } + } + } + + .ingestion-key-details { + border-top: 1px solid var(--bg-vanilla-200); + + .ingestion-key-tag { + background: var(--bg-vanilla-200); + + .tag-text { + color: var(--bg-ink-500); + } + } + + .ingestion-key-created-by { + color: var(--bg-ink-500); + } + + .ingestion-key-last-used-at { + .ant-typography { + color: var(--bg-ink-500); + } + } + } + } + } + } + } + + .delete-ingestion-key-modal { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + + .ant-modal-header { + background: var(--bg-vanilla-100); + + .title { + color: var(--bg-ink-500); + } + } + + .ant-modal-body { + .ant-typography { + color: var(--bg-ink-500); + } + + .ingestion-key-input { + .ant-input { + background: var(--bg-vanilla-200); + color: var(--bg-ink-500); + } + } + } + + .ant-modal-footer { + .cancel-btn { + background: var(--bg-vanilla-300); + color: var(--bg-ink-400); + } + } + } + } + + .ingestion-key-info-container { + .user-email { + background: var(--bg-vanilla-200); + } + + .limits-data { + border: 1px solid var(--bg-vanilla-300); + } + } + + .ingestion-key-modal { + .ant-modal-content { + border-radius: 4px; + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0; + + .ant-modal-header { + background: none; + border-bottom: 1px solid var(--bg-vanilla-200); + padding: 16px; + } + } + } + + .ingestion-key-access-role { + .ant-radio-button-wrapper { + &.ant-radio-button-wrapper-checked { + color: var(--bg-ink-400); + background: var(--bg-vanilla-300); + border-color: var(--bg-vanilla-300); + + &:hover { + color: var(--bg-ink-400); + background: var(--bg-vanilla-300); + border-color: var(--bg-vanilla-300); + + &::before { + background-color: var(--bg-vanilla-300); + } + } + + &:focus { + color: var(--bg-ink-400); + background: var(--bg-vanilla-300); + border-color: var(--bg-vanilla-300); + } + } + } + + .tab { + border: 1px solid var(--bg-vanilla-300); + + &::before { + background: var(--bg-vanilla-300); + } + + &.selected { + background: var(--bg-vanilla-300); + } + } + } + + .copyable-text { + background: var(--bg-vanilla-200); + } + + .ingestion-key-expires-at { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-200); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + } + + .expires-at .ant-picker { + border-color: var(--bg-vanilla-300) !important; + } +} diff --git a/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx new file mode 100644 index 0000000000..7d704f0432 --- /dev/null +++ b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx @@ -0,0 +1,1142 @@ +import './IngestionSettings.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { + Button, + Col, + Collapse, + DatePicker, + Form, + Input, + InputNumber, + Modal, + Row, + Select, + Table, + TablePaginationConfig, + TableProps as AntDTableProps, + Tag, + Typography, +} from 'antd'; +import { NotificationInstance } from 'antd/es/notification/interface'; +import { CollapseProps } from 'antd/lib'; +import createIngestionKeyApi from 'api/IngestionKeys/createIngestionKey'; +import deleteIngestionKey from 'api/IngestionKeys/deleteIngestionKey'; +import createLimitForIngestionKeyApi from 'api/IngestionKeys/limits/createLimitsForKey'; +import deleteLimitsForIngestionKey from 'api/IngestionKeys/limits/deleteLimitsForIngestionKey'; +import updateLimitForIngestionKeyApi from 'api/IngestionKeys/limits/updateLimitsForIngestionKey'; +import updateIngestionKey from 'api/IngestionKeys/updateIngestionKey'; +import { AxiosError } from 'axios'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; +import Tags from 'components/Tags/Tags'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import dayjs, { Dayjs } from 'dayjs'; +import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys'; +import useDebouncedFn from 'hooks/useDebouncedFunction'; +import { useNotifications } from 'hooks/useNotifications'; +import { + CalendarClock, + Check, + Copy, + Infinity, + Minus, + PenLine, + Plus, + PlusIcon, + Search, + Trash2, + X, +} from 'lucide-react'; +import { ChangeEvent, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation } from 'react-query'; +import { useSelector } from 'react-redux'; +import { useCopyToClipboard } from 'react-use'; +import { AppState } from 'store/reducers'; +import { ErrorResponse } from 'types/api'; +import { LimitProps } from 'types/api/ingestionKeys/limits/types'; +import { + IngestionKeyProps, + PaginationProps, +} from 'types/api/ingestionKeys/types'; +import AppReducer from 'types/reducer/app'; +import { USER_ROLES } from 'types/roles'; + +const { Option } = Select; + +const BYTES = 1073741824; + +export const disabledDate = (current: Dayjs): boolean => + // Disable all dates before today + current && current < dayjs().endOf('day'); + +const SIGNALS = ['logs', 'traces', 'metrics']; + +export const showErrorNotification = ( + notifications: NotificationInstance, + err: Error, +): void => { + notifications.error({ + message: err.message || SOMETHING_WENT_WRONG, + }); +}; + +type ExpiryOption = { + value: string; + label: string; +}; + +export const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [ + { value: '1', label: '1 day' }, + { value: '7', label: '1 week' }, + { value: '30', label: '1 month' }, + { value: '90', label: '3 months' }, + { value: '365', label: '1 year' }, + { value: '0', label: 'No Expiry' }, +]; + +function MultiIngestionSettings(): JSX.Element { + const { user } = useSelector((state) => state.app); + const { notifications } = useNotifications(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isDeleteLimitModalOpen, setIsDeleteLimitModalOpen] = useState(false); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const [, handleCopyToClipboard] = useCopyToClipboard(); + const [updatedTags, setUpdatedTags] = useState([]); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isEditAddLimitOpen, setIsEditAddLimitOpen] = useState(false); + const [activeAPIKey, setActiveAPIKey] = useState(); + const [activeSignal, setActiveSignal] = useState(null); + + const [searchValue, setSearchValue] = useState(''); + const [searchText, setSearchText] = useState(''); + const [dataSource, setDataSource] = useState([]); + const [paginationParams, setPaginationParams] = useState({ + page: 1, + per_page: 10, + }); + + const [totalIngestionKeys, setTotalIngestionKeys] = useState(0); + + const [ + hasCreateLimitForIngestionKeyError, + setHasCreateLimitForIngestionKeyError, + ] = useState(false); + + const [ + createLimitForIngestionKeyError, + setCreateLimitForIngestionKeyError, + ] = useState(null); + + const [ + hasUpdateLimitForIngestionKeyError, + setHasUpdateLimitForIngestionKeyError, + ] = useState(false); + + const [ + updateLimitForIngestionKeyError, + setUpdateLimitForIngestionKeyError, + ] = useState(null); + + const { t } = useTranslation(['ingestionKeys']); + + const [editForm] = Form.useForm(); + const [addEditLimitForm] = Form.useForm(); + const [createForm] = Form.useForm(); + + const handleFormReset = (): void => { + editForm.resetFields(); + createForm.resetFields(); + addEditLimitForm.resetFields(); + }; + + const hideDeleteViewModal = (): void => { + setIsDeleteModalOpen(false); + setActiveAPIKey(null); + handleFormReset(); + }; + + const showDeleteModal = (apiKey: IngestionKeyProps): void => { + setActiveAPIKey(apiKey); + setIsDeleteModalOpen(true); + }; + + const hideEditViewModal = (): void => { + setActiveAPIKey(null); + setIsEditModalOpen(false); + handleFormReset(); + }; + + const hideAddViewModal = (): void => { + handleFormReset(); + setActiveAPIKey(null); + setIsAddModalOpen(false); + }; + + const showEditModal = (apiKey: IngestionKeyProps): void => { + setActiveAPIKey(apiKey); + + handleFormReset(); + setUpdatedTags(apiKey.tags || []); + + editForm.setFieldsValue({ + name: apiKey.name, + tags: apiKey.tags, + expires_at: dayjs(apiKey?.expires_at) || null, + }); + + setIsEditModalOpen(true); + }; + + const showAddModal = (): void => { + setUpdatedTags([]); + setActiveAPIKey(null); + setIsAddModalOpen(true); + }; + + const handleModalClose = (): void => { + setActiveAPIKey(null); + setActiveSignal(null); + }; + + const { + data: IngestionKeys, + isLoading, + isRefetching, + refetch: refetchAPIKeys, + error, + isError, + } = useGetAllIngestionsKeys({ + search: searchText, + ...paginationParams, + }); + + useEffect(() => { + setActiveAPIKey(IngestionKeys?.data.data[0]); + }, [IngestionKeys]); + + useEffect(() => { + setDataSource(IngestionKeys?.data.data || []); + setTotalIngestionKeys(IngestionKeys?.data?._pagination?.total || 0); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [IngestionKeys?.data?.data]); + + useEffect(() => { + if (isError) { + showErrorNotification(notifications, error as AxiosError); + } + }, [error, isError, notifications]); + + const handleDebouncedSearch = useDebouncedFn((searchText): void => { + setSearchText(searchText as string); + }, 500); + + const handleSearch = (e: ChangeEvent): void => { + setSearchValue(e.target.value); + handleDebouncedSearch(e.target.value || ''); + }; + + const clearSearch = (): void => { + setSearchValue(''); + }; + + const { + mutate: createIngestionKey, + isLoading: isLoadingCreateAPIKey, + } = useMutation(createIngestionKeyApi, { + onSuccess: (data) => { + setActiveAPIKey(data.payload); + setUpdatedTags([]); + hideAddViewModal(); + refetchAPIKeys(); + }, + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); + }, + }); + + const { mutate: updateAPIKey, isLoading: isLoadingUpdateAPIKey } = useMutation( + updateIngestionKey, + { + onSuccess: () => { + refetchAPIKeys(); + setIsEditModalOpen(false); + }, + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); + }, + }, + ); + + const { mutate: deleteAPIKey, isLoading: isDeleteingAPIKey } = useMutation( + deleteIngestionKey, + { + onSuccess: () => { + refetchAPIKeys(); + setIsDeleteModalOpen(false); + }, + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); + }, + }, + ); + + const { + mutate: createLimitForIngestionKey, + isLoading: isLoadingLimitForKey, + } = useMutation(createLimitForIngestionKeyApi, { + onSuccess: () => { + setActiveSignal(null); + setActiveAPIKey(null); + setIsEditAddLimitOpen(false); + setUpdatedTags([]); + hideAddViewModal(); + refetchAPIKeys(); + setHasCreateLimitForIngestionKeyError(false); + }, + onError: (error: ErrorResponse) => { + setHasCreateLimitForIngestionKeyError(true); + setCreateLimitForIngestionKeyError(error); + }, + }); + + const { + mutate: updateLimitForIngestionKey, + isLoading: isLoadingUpdatedLimitForKey, + } = useMutation(updateLimitForIngestionKeyApi, { + onSuccess: () => { + setActiveSignal(null); + setActiveAPIKey(null); + setIsEditAddLimitOpen(false); + setUpdatedTags([]); + hideAddViewModal(); + refetchAPIKeys(); + setHasUpdateLimitForIngestionKeyError(false); + }, + onError: (error: ErrorResponse) => { + setHasUpdateLimitForIngestionKeyError(true); + setUpdateLimitForIngestionKeyError(error); + }, + }); + + const { mutate: deleteLimitForKey, isLoading: isDeletingLimit } = useMutation( + deleteLimitsForIngestionKey, + { + onSuccess: () => { + setIsDeleteModalOpen(false); + setIsDeleteLimitModalOpen(false); + refetchAPIKeys(); + }, + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); + }, + }, + ); + + const onDeleteHandler = (): void => { + clearSearch(); + + if (activeAPIKey) { + deleteAPIKey(activeAPIKey.id); + } + }; + + const onUpdateApiKey = (): void => { + editForm + .validateFields() + .then((values) => { + if (activeAPIKey) { + updateAPIKey({ + id: activeAPIKey.id, + data: { + name: values.name, + tags: updatedTags, + expires_at: dayjs(values.expires_at).endOf('day').toISOString(), + }, + }); + } + }) + .catch((errorInfo) => { + console.error('error info', errorInfo); + }); + }; + + const onCreateIngestionKey = (): void => { + createForm + .validateFields() + .then((values) => { + if (user) { + const requestPayload = { + name: values.name, + tags: updatedTags, + expires_at: dayjs(values.expires_at).endOf('day').toISOString(), + }; + + createIngestionKey(requestPayload); + } + }) + .catch((errorInfo) => { + console.error('error info', errorInfo); + }); + }; + + const handleCopyKey = (text: string): void => { + handleCopyToClipboard(text); + notifications.success({ + message: 'Copied to clipboard', + }); + }; + + const gbToBytes = (gb: number): number => Math.round(gb * 1024 ** 3); + + const getFormattedTime = (date: string): string => + dayjs(date).format('MMM DD,YYYY, hh:mm a'); + + const handleAddLimit = ( + APIKey: IngestionKeyProps, + signalName: string, + ): void => { + setActiveSignal({ + id: signalName, + signal: signalName, + config: {}, + }); + + const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); + + const payload = { + keyID: APIKey.id, + signal: signalName, + config: { + day: { + size: gbToBytes(dailyLimit), + }, + second: { + size: gbToBytes(secondsLimit), + }, + }, + }; + + createLimitForIngestionKey(payload); + }; + + const handleUpdateLimit = ( + APIKey: IngestionKeyProps, + signal: LimitProps, + ): void => { + setActiveSignal(signal); + const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); + const payload = { + limitID: signal.id, + signal: signal.signal, + config: { + day: { + size: gbToBytes(dailyLimit), + }, + second: { + size: gbToBytes(secondsLimit), + }, + }, + }; + updateLimitForIngestionKey(payload); + }; + + const bytesToGb = (size: number | undefined): number => { + if (!size) { + return 0; + } + + return size / BYTES; + }; + + const enableEditLimitMode = ( + APIKey: IngestionKeyProps, + signal: LimitProps, + ): void => { + setActiveAPIKey(APIKey); + setActiveSignal(signal); + + addEditLimitForm.setFieldsValue({ + dailyLimit: bytesToGb(signal?.config?.day?.size || 0), + secondsLimit: bytesToGb(signal?.config?.second?.size || 0), + }); + + setIsEditAddLimitOpen(true); + }; + + const onDeleteLimitHandler = (): void => { + if (activeSignal && activeSignal?.id) { + deleteLimitForKey(activeSignal.id); + } + }; + + const showDeleteLimitModal = ( + APIKey: IngestionKeyProps, + limit: LimitProps, + ): void => { + setActiveAPIKey(APIKey); + setActiveSignal(limit); + setIsDeleteLimitModalOpen(true); + }; + + const hideDeleteLimitModal = (): void => { + setIsDeleteLimitModalOpen(false); + }; + + const handleDiscardSaveLimit = (): void => { + setHasCreateLimitForIngestionKeyError(false); + setHasUpdateLimitForIngestionKeyError(false); + setIsEditAddLimitOpen(false); + setActiveAPIKey(null); + setActiveSignal(null); + + addEditLimitForm.resetFields(); + }; + + const columns: AntDTableProps['columns'] = [ + { + title: 'Ingestion Key', + key: 'ingestion-key', + // eslint-disable-next-line sonarjs/cognitive-complexity + render: (APIKey: IngestionKeyProps): JSX.Element => { + const createdOn = getFormattedTime(APIKey.created_at); + const formattedDateAndTime = + APIKey && APIKey?.expires_at && getFormattedTime(APIKey?.expires_at); + + const updatedOn = getFormattedTime(APIKey?.updated_at); + + const limits: { [key: string]: LimitProps } = {}; + + APIKey.limits?.forEach((limit: LimitProps) => { + limits[limit.signal] = limit; + }); + + const hasLimits = (signal: string): boolean => !!limits[signal]; + + const items: CollapseProps['items'] = [ + { + key: '1', + label: ( +
+
+
+ {APIKey?.name} +
+ +
+ + {APIKey?.value.substring(0, 2)}******** + {APIKey?.value.substring(APIKey.value.length - 2).trim()} + + + { + e.stopPropagation(); + e.preventDefault(); + handleCopyKey(APIKey.value); + }} + /> +
+
+
+
+
+ ), + children: ( +
+ + Created on + + {createdOn} + + + + {updatedOn && ( + + Updated on + + {updatedOn} + + + )} + + {APIKey.tags && Array.isArray(APIKey.tags) && APIKey.tags.length > 0 && ( + + Tags + +
+
+ {APIKey.tags.map((tag, index) => ( + // eslint-disable-next-line react/no-array-index-key + {tag} + ))} +
+
+ +
+ )} + +
+

LIMITS

+ +
+
+ {SIGNALS.map((signal) => ( +
+
+
{signal}
+
+ {hasLimits(signal) ? ( + <> + + )} +
+
+ +
+ {activeAPIKey?.id === APIKey.id && + activeSignal?.signal === signal && + isEditAddLimitOpen ? ( +
+
+
+
+
Daily limit
+
+ Add a limit for data ingested daily{' '} +
+
+ +
+ + + + + + + + } + /> + +
+
+ +
+
+
Per Second limit
+
+ {' '} + Add a limit for data ingested every second{' '} +
+
+ +
+ + + + + + + + } + /> + +
+
+
+ + {activeAPIKey?.id === APIKey.id && + activeSignal.signal === signal && + !isLoadingLimitForKey && + hasCreateLimitForIngestionKeyError && + createLimitForIngestionKeyError && + createLimitForIngestionKeyError?.error && ( +
+ {createLimitForIngestionKeyError?.error} +
+ )} + + {activeAPIKey?.id === APIKey.id && + activeSignal.signal === signal && + !isLoadingLimitForKey && + hasUpdateLimitForIngestionKeyError && + updateLimitForIngestionKeyError && ( +
+ {updateLimitForIngestionKeyError?.error} +
+ )} + + {activeAPIKey?.id === APIKey.id && + activeSignal.signal === signal && + isEditAddLimitOpen && ( +
+ + +
+ )} +
+ ) : ( +
+
+
+ Daily {' '} +
+ +
+ {limits[signal]?.config?.day?.size ? ( + <> + {getYAxisFormattedValue( + (limits[signal]?.metric?.day?.size || 0).toString(), + 'bytes', + )}{' '} + /{' '} + {getYAxisFormattedValue( + (limits[signal]?.config?.day?.size || 0).toString(), + 'bytes', + )} + + ) : ( + <> + NO LIMIT + + )} +
+
+ +
+
+ Seconds +
+ +
+ {limits[signal]?.config?.second?.size ? ( + <> + {getYAxisFormattedValue( + (limits[signal]?.metric?.second?.size || 0).toString(), + 'bytes', + )}{' '} + /{' '} + {getYAxisFormattedValue( + (limits[signal]?.config?.second?.size || 0).toString(), + 'bytes', + )} + + ) : ( + <> + NO LIMIT + + )} +
+
+
+ )} +
+
+ ))} +
+
+
+
+ ), + }, + ]; + + return ( +
+ + +
+
+ + Expires on + {formattedDateAndTime} +
+
+
+ ); + }, + }, + ]; + + const handleTableChange = (pagination: TablePaginationConfig): void => { + setPaginationParams({ + page: pagination?.current || 1, + per_page: 10, + }); + }; + + return ( +
+
+
+ Ingestion Keys + + Create and manage ingestion keys for the SigNoz Cloud + +
+ +
+ } + value={searchValue} + onChange={handleSearch} + /> + + +
+ + + `${range[0]}-${range[1]} of ${total} Ingestion keys`, + total: totalIngestionKeys, + }} + /> + + + {/* Delete Key Modal */} + Delete Ingestion Key} + open={isDeleteModalOpen} + closable + afterClose={handleModalClose} + onCancel={hideDeleteViewModal} + destroyOnClose + footer={[ + , + , + ]} + > + + {t('delete_confirm_message', { + keyName: activeAPIKey?.name, + })} + + + + {/* Delete Limit Modal */} + Delete Limit } + open={isDeleteLimitModalOpen} + closable + afterClose={handleModalClose} + onCancel={hideDeleteLimitModal} + destroyOnClose + footer={[ + , + , + ]} + > + + {t('delete_limit_confirm_message', { + limit_name: activeSignal?.signal, + keyName: activeAPIKey?.name, + })} + + + + {/* Edit Modal */} + } + > + Cancel + , + , + ]} + > +
+ + + + + + + + + + + + +
+ + {/* Create New Key Modal */} + } + > + Cancel + , + , + ]} + > +
+ + + + + + + + + + + + +
+ + ); +} + +export default MultiIngestionSettings; diff --git a/frontend/src/container/ListAlertRules/ListAlert.tsx b/frontend/src/container/ListAlertRules/ListAlert.tsx index 39465e28a8..37a56ba921 100644 --- a/frontend/src/container/ListAlertRules/ListAlert.tsx +++ b/frontend/src/container/ListAlertRules/ListAlert.tsx @@ -117,14 +117,19 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { .refetch() .then(() => { const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery); - - history.push( - `${ROUTES.EDIT_ALERTS}?ruleId=${record.id.toString()}&${ - QueryParams.compositeQuery - }=${encodeURIComponent(JSON.stringify(compositeQuery))}&panelTypes=${ - record.condition.compositeQuery.panelType - }`, + params.set( + QueryParams.compositeQuery, + encodeURIComponent(JSON.stringify(compositeQuery)), ); + + params.set( + QueryParams.panelTypes, + record.condition.compositeQuery.panelType, + ); + + params.set(QueryParams.ruleId, record.id.toString()); + + history.push(`${ROUTES.EDIT_ALERTS}?${params.toString()}`); }) .catch(handleError); }; @@ -151,7 +156,8 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { setData(refetchData.payload || []); setTimeout(() => { const clonedAlert = refetchData.payload[refetchData.payload.length - 1]; - history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${clonedAlert.id}`); + params.set(QueryParams.ruleId, String(clonedAlert.id)); + history.push(`${ROUTES.EDIT_ALERTS}?${params.toString()}`); }, 2000); } if (status === 'error') { diff --git a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss new file mode 100644 index 0000000000..11dac46fde --- /dev/null +++ b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss @@ -0,0 +1,1315 @@ +.dashboards-list-container { + margin-top: 30px; + margin-bottom: 30px; + display: flex; + justify-content: center; + width: 100%; + + .dashboards-list-view-content { + width: calc(100% - 30px); + max-width: 836px; + + .dashboards-list-title-container { + .title { + color: var(--bg-vanilla-100); + font-size: var(--font-size-lg); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 28px; /* 155.556% */ + letter-spacing: -0.09px; + } + + .subtitle { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + + .ant-table-row { + .ant-table-cell { + padding: 0; + border: none; + background: var(--bg-ink-500); + } + + .dashboard-list-item { + padding: 12px 16px 16px 16px; + border: 1px solid var(--bg-slate-500); + border-top: none; + background: var(--bg-ink-400); + cursor: pointer; + + .title-with-action { + display: flex; + justify-content: space-between; + align-items: center; + + min-height: 24px; + + .dashboard-title { + display: flex; + align-items: center; + gap: 6px; + line-height: 20px; + + .dot { + min-height: 6px; + min-width: 6px; + border-radius: 50%; + } + + .ant-typography { + color: var(--bg-vanilla-100); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + letter-spacing: -0.07px; + } + } + + .action-btn { + display: flex; + align-items: center; + gap: 20px; + cursor: pointer; + + .hidden { + display: none; + } + } + } + + .dashboard-details { + margin-top: 12px; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px 24px; + + .dashboard-tag { + width: 14px; + height: 14px; + border-radius: 50px; + background: var(--bg-slate-300); + display: flex; + justify-content: center; + align-items: center; + + .tag-text { + color: var(--bg-vanilla-400); + leading-trim: both; + text-edge: cap; + font-size: 8px; + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: normal; + letter-spacing: -0.05px; + } + } + + .created-by { + display: flex; + align-items: center; + + .dashboard-tag { + width: 14px; + height: 14px; + border-radius: 50px; + background: var(--bg-slate-300); + display: flex; + justify-content: center; + align-items: center; + + .tag-text { + color: var(--bg-vanilla-400); + leading-trim: both; + text-edge: cap; + font-size: 8px; + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: normal; + letter-spacing: -0.05px; + } + } + .dashboard-created-by { + margin-left: 8px; + color: var(--Vanilla-400, #c0c1c3); + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + } + + .dashboard-created-at { + display: flex; + align-items: center; + + .ant-typography { + margin-left: 6px; + color: var(--bg-vanilla-400); + font-size: var(--font-size-xs); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + color: var(--Vanilla-400, #c0c1c3); + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + } + + .updated-by { + display: flex; + align-items: center; + + .text { + color: var(--bg-vanilla-400); + font-size: var(--font-size-xs); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + color: var(--Vanilla-400, #c0c1c3); + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + + .dashboard-tag { + width: 14px; + height: 14px; + border-radius: 50px; + background: var(--bg-slate-300); + display: flex; + justify-content: center; + align-items: center; + + .tag-text { + color: var(--bg-vanilla-400); + leading-trim: both; + text-edge: cap; + font-size: 8px; + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: normal; + letter-spacing: -0.05px; + } + } + .dashboard-created-by { + margin-left: 8px; + color: var(--Vanilla-400, #c0c1c3); + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + } + } + } + } + + .ant-pagination-item { + display: flex; + justify-content: center; + align-items: center; + + > a { + color: var(--bg-vanilla-400); + font-variant-numeric: lining-nums tabular-nums slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'case' on, 'cpsp' on; + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; /* 142.857% */ + } + } + + .ant-pagination-item-active { + background-color: var(--bg-robin-500); + > a { + color: var(--bg-ink-500) !important; + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + } + } + + .ant-table-tbody { + .ant-table-row:last-child { + .dashboard-list-item { + border-radius: 0px 0px 6px 6px; + } + } + } + } + + .dashboards-list-header-container { + display: flex; + align-items: center; + gap: 8px; + + margin: 16px 0; + + .btn { + padding: 5.937px 11.875px; + } + } + + .ant-pagination-total-text { + display: flex; + gap: 4px; + align-items: center; + + .numbers { + font-family: 'Space Mono'; + } + + .total { + font-family: 'Space Mono'; + color: var(--bg-vanilla-300); + } + } + + .dashboard-error-state { + display: flex; + flex-direction: column; + height: 320px; + padding: 105px 141px; + margin-top: 16px; + justify-content: center; + align-items: flex-start; + border-radius: 6px; + border: 1px dashed var(--bg-slate-500); + gap: 4px; + + .error-text { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + + .action-btns { + display: flex; + gap: 24px; + align-items: center; + margin-top: 20px; + + .retry-btn { + display: flex; + align-items: center; + height: 32px; + padding: 5.937px 11.875px; + justify-content: center; + gap: 5.937px; + border-radius: 1.484px; + background: var(--bg-slate-500); + color: var(--bg-vanilla-100); + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + + .learn-more { + color: var(--bg-robin-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + padding: 0px; + } + + .learn-more:hover { + &.ant-btn-text { + background-color: unset; + } + } + .learn-more-arrow { + margin-left: -20px; + color: var(--bg-robin-400); + cursor: pointer; + } + } + } + + .dashboard-empty-state { + display: flex; + flex-direction: column; + height: 320px; + padding: 105px 141px; + margin-top: 16px; + justify-content: center; + align-items: flex-start; + border-radius: 6px; + border: 1px dashed var(--bg-slate-500); + + .dashboard-img { + width: 32px; + height: 32px; + } + + .text { + margin-top: 4px; + .no-dashboard { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + .info { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + } + + .actions { + display: flex; + gap: 24px; + align-items: center; + margin-top: 24px; + + .new-dashboard { + display: flex; + width: 153px; + align-items: center; + height: 32px; + padding: 5.937px 11.875px; + justify-content: center; + gap: 5.937px; + border-radius: 1.484px; + background: var(--bg-robin-500); + color: var(--bg-vanilla-100); + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + .learn-more { + color: var(--bg-robin-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + padding: 0px; + } + + .learn-more:hover { + &.ant-btn-text { + background-color: unset; + } + } + .learn-more-arrow { + margin-left: -20px; + color: var(--bg-robin-400); + cursor: pointer; + } + } + } + + .loading-dashboard-details { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 16px; + + .skeleton-1 { + height: 125px; + width: 100%; + } + } + + .no-search { + display: flex; + flex-direction: column; + height: 320px; + gap: 8px; + padding: 105px 190px; + margin-top: 16px; + justify-content: center; + align-items: flex-start; + border-radius: 6px; + border: 1px dashed var(--bg-slate-500); + + .img { + width: 32px; + height: 32px; + } + + .text { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + } + + .all-dashboards-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + height: 44px; + flex-shrink: 0; + border-radius: 6px 6px 0px 0px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1); + + .typography { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .right-actions { + display: flex; + gap: 12px; + color: white; + cursor: pointer; + } + } + + .tags-with-actions { + display: flex; + align-items: center; + + .dashboard-tags { + display: flex; + + .tag { + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + height: 28px; + border-radius: 20px; + border: 1px solid rgba(173, 127, 88, 0.2); + background: rgba(173, 127, 88, 0.1); + color: var(--bg-sienna-400); + text-align: center; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + } +} + +.new-dashboard-menu { + .create-dashboard-menu-item { + display: flex; + align-items: center; + gap: 8px; + } +} + +.delete-view-modal { + width: calc(100% - 30px) !important; /* Adjust the 20px as needed */ + max-width: 384px; + .ant-modal-content { + padding: 0; + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + + .ant-modal-header { + padding: 16px; + background: var(--bg-ink-400); + } + + .ant-modal-body { + padding: 0px 16px 28px 16px; + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; + letter-spacing: -0.07px; + } + + .save-view-input { + margin-top: 8px; + display: flex; + gap: 8px; + } + + .ant-color-picker-trigger { + padding: 6px; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + width: 32px; + height: 32px; + + .ant-color-picker-color-block { + border-radius: 50px; + width: 16px; + height: 16px; + flex-shrink: 0; + + .ant-color-picker-color-block-inner { + display: flex; + justify-content: center; + align-items: center; + } + } + } + } + + .ant-modal-footer { + display: flex; + justify-content: flex-end; + padding: 16px 16px; + margin: 0; + + .cancel-btn { + display: flex; + align-items: center; + border: none; + border-radius: 2px; + background: var(--bg-slate-500); + } + + .delete-btn { + display: flex; + align-items: center; + border: none; + border-radius: 2px; + background: var(--bg-cherry-500); + margin-left: 12px; + } + + .delete-btn:hover { + color: var(--bg-vanilla-100); + background: var(--bg-cherry-600); + } + } + } + .title { + color: var(--bg-vanilla-100); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; /* 142.857% */ + } +} + +.dashboard-actions { + .ant-popover-inner { + width: 187px; + height: auto; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--bg-slate-400); + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ); + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px); + padding: 0px; + + .dashboard-action-content { + .section-1 { + display: flex; + flex-direction: column; + + .action-btn { + display: flex; + padding: 14px; + height: unset; + align-items: center; + gap: 6px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + + .ant-icon-btn { + margin-inline-end: 0px; + } + } + } + + .section-2 { + display: flex; + flex-direction: column; + border-top: 1px solid var(--bg-slate-400); + + .ant-typography { + display: flex; + padding: 14px; + align-items: center; + gap: 6px; + color: var(--bg-cherry-400) !important; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + } + } + } + } +} + +.sort-dashboards { + .ant-popover-inner { + display: flex; + padding: 0px; + align-items: center; + border-radius: 4px; + border: 1px solid var(--bg-slate-400); + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ); + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px); + gap: 16px; + + .sort-content { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 140px; + + .sort-heading { + color: var(--bg-slate-200); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + padding: 12px 18px 6px 14px; + } + + .sort-btns { + text-align: start; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + padding: 12px 18px 12px 14px; + height: auto; + } + } + } +} + +.configure-group { + .ant-popover-inner { + display: flex; + align-items: center; + border-radius: 4px; + padding: 0px; + border: 1px solid var(--Slate-400, #1d212d); + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ); + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px); + gap: 16px; + .configure-content { + display: flex; + flex-direction: column; + align-items: flex-start; + .configure-btn { + display: flex; + text-align: start; + align-items: center; + width: 100%; + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + padding: 12px; + } + } + } +} + +.configure-metadata-root { + .ant-modal-content { + width: 400px; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--Slate-500, #161922); + background: var(--Ink-400, #121317); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0px; + + .ant-modal-header { + background: var(--Ink-400, #121317); + padding: 16px; + border-bottom: 1px solid var(--bg-slate-500); + margin-bottom: 0px; + } + + .ant-modal-body { + padding: 14px 16px; + + .configure-content { + display: flex; + flex-direction: column; + gap: 14px; + + .configure-preview { + display: flex; + padding: 12px 14.634px; + flex-direction: column; + align-items: flex-start; + gap: 7.317px; + border-radius: 4px; + border: 0.915px solid var(--Slate-500, #161922); + background: var(--Ink-300, #16181d); + + .header { + display: flex; + gap: 4px; + + .title { + color: var(--Vanilla-100, #fff); + font-family: Inter; + font-size: 12.805px; + font-style: normal; + font-weight: 500; + line-height: 18.293px; /* 142.857% */ + letter-spacing: -0.064px; + } + } + + .details { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + + .createdAt { + display: flex; + justify-content: space-between; + align-items: center; + + .formatted-time { + display: flex; + gap: 4px; + color: var(--Vanilla-400, #c0c1c3); + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; + font-family: Inter; + font-size: 12.805px; + font-style: normal; + font-weight: 400; + line-height: 16.463px; /* 128.571% */ + letter-spacing: -0.064px; + } + + .user { + display: flex; + align-items: center; + gap: 4px; + + .user-tag { + width: 12px; + height: 12px; + display: flex; + justify-content: center; + align-items: center; + color: var(--bg-vanilla-400); + font-size: 8px; + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: normal; + letter-spacing: -0.05px; + border-radius: 12.805px; + background-color: var(--bg-ink-500); + } + + .dashboard-created-by { + color: var(--Vanilla-400, #c0c1c3); + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; + font-family: Inter; + font-size: 12.805px; + font-style: normal; + font-weight: 400; + line-height: 16.463px; /* 128.571% */ + letter-spacing: -0.064px; + } + } + } + + .updatedAt { + display: flex; + justify-content: space-between; + align-items: center; + + .formatted-time { + display: flex; + gap: 4px; + color: var(--Vanilla-400, #c0c1c3); + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; + font-family: Inter; + font-size: 12.805px; + font-style: normal; + font-weight: 400; + line-height: 16.463px; /* 128.571% */ + letter-spacing: -0.064px; + } + + .user { + display: flex; + align-items: center; + gap: 4px; + + .user-tag { + width: 12px; + height: 12px; + display: flex; + justify-content: center; + align-items: center; + color: var(--bg-vanilla-400); + font-size: 8px; + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: normal; + letter-spacing: -0.05px; + border-radius: 12.805px; + background-color: var(--bg-ink-500); + } + + .dashboard-created-by { + color: var(--Vanilla-400, #c0c1c3); + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; + font-family: Inter; + font-size: 12.805px; + font-style: normal; + font-weight: 400; + line-height: 16.463px; /* 128.571% */ + letter-spacing: -0.064px; + } + } + } + } + } + + .metadata-action { + display: flex; + justify-content: space-between; + align-items: center; + width: 336px; + padding: 0px 0px 0px 14.634px; + + .left { + display: flex; + gap: 6px; + align-items: center; + } + + .connection-line { + border: 1px dashed var(--bg-slate-400); + min-width: 20px; + height: 0px; + flex-grow: 1; + margin: 0px 8px; + } + } + } + } + + .ant-modal-footer { + margin-top: 0px; + padding: 4px 16px 16px 16px; + + .save-changes { + display: flex; + width: 100%; + height: 32px; + padding: 8px 16px; + justify-content: center; + align-items: center; + flex-shrink: 0; + border-radius: 2px; + border: 1px solid var(--Slate-300, #242834); + background: var(--Slate-400, #1d212d); + } + } + } +} + +.lightMode { + .dashboards-list-container { + .dashboards-list-view-content { + .title { + color: var(--bg-ink-500); + } + .subtitle { + color: var(--bg-vanilla-400); + } + + .ant-table-row { + .ant-table-cell { + background: var(--bg-vanilla-200); + } + + &:hover { + .ant-table-cell { + background: var(--bg-vanilla-200) !important; + } + } + + .dashboard-list-item { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + + .title-with-action { + .dashboard-title { + .ant-typography { + color: var(--bg-ink-500); + } + } + + .action-btn { + .ant-typography { + color: var(--bg-ink-500); + } + } + } + + .dashboard-details { + .dashboard-tag { + background: var(--bg-vanilla-200); + .tag-text { + color: var(--bg-ink-500); + } + } + .created-by { + .dashboard-tag { + background: var(--bg-vanilla-200); + + .tag-text { + color: var(--bg-ink-500); + } + } + .dashboard-created-by { + color: var(--bg-ink-400); + } + } + + .updated-by { + .text { + color: var(--bg-ink-400); + } + + .dashboard-tag { + background: var(--bg-vanilla-200); + + .tag-text { + color: var(--bg-ink-500); + } + } + .dashboard-created-by { + color: var(--bg-ink-400); + } + } + + .dashboard-created-by { + color: var(--bg-ink-500); + } + + .dashboard-created-at { + .ant-typography { + color: var(--bg-ink-500); + } + } + } + } + } + } + .no-search { + .text { + color: var(--bg-ink-300); + } + } + + .all-dashboards-header { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .typography { + color: var(--bg-ink-400); + } + + .right-actions { + color: var(--bg-ink-100); + } + } + .dashboard-empty-state { + .text { + .no-dashboard { + color: var(--bg-ink-100); + } + .info { + color: var(--bg-vanilla-400); + } + } + } + .dashboard-error-state { + .error-text { + color: var(--bg-ink-300); + } + + .action-btns { + .retry-btn { + background: var(--bg-vanilla-300); + color: var(--bg-ink-300); + } + } + } + } + + .delete-view-modal { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + + .ant-modal-header { + background: var(--bg-vanilla-100); + + .title { + color: var(--bg-ink-500); + } + } + + .ant-modal-body { + .ant-typography { + color: var(--bg-ink-500); + } + + .save-view-input { + .ant-input { + background: var(--bg-vanilla-200); + color: var(--bg-ink-500); + } + } + } + + .ant-modal-footer { + .cancel-btn { + background: var(--bg-vanilla-300); + color: var(--bg-ink-400); + } + } + } + } + + .dashboard-actions { + .ant-popover-inner { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .dashboard-action-content { + .section-1 { + .action-btn { + color: var(--bg-ink-400); + } + } + + .section-2 { + border-top: 1px solid var(--bg-vanilla-300); + } + } + } + } + + .sort-dashboards { + .ant-popover-inner { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .sort-content { + .sort-heading { + color: var(--bg-vanilla-400); + } + + .sort-btns { + color: var(--bg-ink-400); + } + } + } + } + + .configure-group { + .ant-popover-inner { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + .configure-content { + .configure-btn { + color: var(--bg-ink-400); + } + } + } + } + + .configure-metadata-root { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .ant-modal-header { + background: var(--bg-vanilla-100); + border-bottom: 1px solid var(--bg-vanilla-300); + } + + .ant-modal-body { + .configure-content { + .configure-preview { + border: 0.915px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .header { + .title { + color: var(--bg-ink-400); + } + } + + .details { + .createdAt { + .formatted-time { + color: var(--bg-ink-400); + } + + .user { + .user-tag { + color: var(--bg-ink-400); + background-color: var(--bg-vanilla-300); + } + + .dashboard-created-by { + color: var(--bg-ink-400); + } + } + } + + .updatedAt { + .formatted-time { + color: var(--bg-ink-400); + } + + .user { + .user-tag { + color: var(--bg-ink-400); + background-color: var(--bg-vanilla-300); + } + + .dashboard-created-by { + color: var(--bg-ink-400); + } + } + } + } + } + + .metadata-action { + .connection-line { + border: 1px dashed var(--bg-vanilla-300); + } + } + } + } + + .ant-modal-footer { + .save-changes { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-200); + } + } + } + } +} diff --git a/frontend/src/container/ListOfDashboard/DashboardTemplates/DashboardTemplatesModal.styles.scss b/frontend/src/container/ListOfDashboard/DashboardTemplates/DashboardTemplatesModal.styles.scss new file mode 100644 index 0000000000..51fb567364 --- /dev/null +++ b/frontend/src/container/ListOfDashboard/DashboardTemplates/DashboardTemplatesModal.styles.scss @@ -0,0 +1,269 @@ +.new-dashboard-templates-modal { + .ant-modal-content { + border-radius: 4px; + border: 1px solid var(--bg-slate-400); + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ); + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px); + + padding: 0; + height: 72vh; + + .ant-modal-body { + height: 100%; + } + } + + .new-dashboard-templates-content-container { + height: 100%; + } + + .new-dashboard-templates-content-header { + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--bg-slate-500); + height: 60px; + box-sizing: border-box; + } + + .new-dashboard-templates-content { + overflow: hidden; + display: flex; + position: relative; + + height: calc(100% - 60px); + + .new-dashboard-templates-list { + padding: 16px 8px; + height: 100%; + width: 25%; + border-right: 1px solid var(--bg-slate-500); + + .new-dashboard-templates-search { + height: 32px; + margin-bottom: 16px; + } + + .templates-list { + display: flex; + flex-direction: column; + gap: 8px; + padding-bottom: 16px; + overflow-y: auto; + box-sizing: border-box; + + height: calc(100% - 64px); + + &::-webkit-scrollbar { + height: 1rem; + width: 0.1rem; + } + + .template-list-item { + display: flex; + gap: 8px; + padding: 4px 12px; + align-items: center; + cursor: pointer; + height: 32px; + box-sizing: border-box; + + .template-icon { + display: flex; + height: 14px; + width: 14px; + align-items: center; + justify-content: center; + } + + .template-name { + color: #c0c1c3; + font-style: normal; + font-weight: 300; + line-height: 18px; + } + + &:hover { + border-radius: 3px; + background: rgba(171, 189, 255, 0.08); + } + + &.active { + border-radius: 3px; + background: rgba(171, 189, 255, 0.08); + } + } + } + } + + .new-dashboard-template-preview { + flex: 1; + position: relative; + + .template-preview-header { + padding: 16px; + + display: flex; + gap: 8px; + align-items: center; + justify-content: space-between; + + .template-preview-title { + display: flex; + justify-content: center; + align-items: center; + gap: 16px; + + .template-preview-icon { + height: 40px; + width: 40px; + flex-shrink: 0; + border-radius: 2px; + border: 1px solid var(--bg-ink-50); + background: var(--bg-ink-300); + display: flex; + align-items: center; + justify-content: center; + } + + .template-info { + .template-name { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .template-description { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + } + } + } + } + + .template-preview-image { + display: flex; + justify-content: center; + align-items: center; + margin: 24px; + height: calc(100% - 144px); + position: relative; + + img { + width: 100%; + max-width: 100%; + padding: 24px; + border: 1px solid var(--bg-ink-50); + background: var(--bg-ink-300); + max-height: 100%; + object-fit: contain; + } + } + } + } + + .new-dashboard-templates-modal-footer { + .create-dashboard-json-error { + margin-bottom: 8px; + display: flex; + } + + .action-btns-container { + display: flex; + justify-content: space-between; + } + } + + .ant-modal-footer { + margin-top: 0; + padding: 16px; + border-top: 1px solid var(--bg-slate-500); + } +} + +.lightMode { + .new-dashboard-templates-modal { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + + .new-dashboard-templates-content-header { + border-bottom: 1px solid var(--bg-vanilla-300); + } + + .new-dashboard-templates-content { + .new-dashboard-templates-list { + border-right: 1px solid var(--bg-vanilla-300); + + .templates-list { + .template-list-item { + .template-name { + color: var(--bg-ink-300); + } + + &:hover { + background: rgba(171, 189, 255, 0.08); + } + + &.active { + background: rgba(171, 189, 255, 0.08); + } + } + } + } + + .new-dashboard-template-preview { + .template-preview-header { + .template-preview-title { + .template-preview-icon { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + + .template-info { + .template-name { + color: var(--bg-ink-300); + } + + .template-description { + color: var(--bg-vanilla-400); + } + } + } + + .create-dashboard-btn { + .ant-btn { + box-shadow: none; + } + } + } + + .template-preview-image { + img { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + } + } + } + + .ant-modal-footer { + border-top: 1px solid var(--bg-vanilla-300); + } + } +} diff --git a/frontend/src/container/ListOfDashboard/DashboardTemplates/DashboardTemplatesModal.tsx b/frontend/src/container/ListOfDashboard/DashboardTemplates/DashboardTemplatesModal.tsx new file mode 100644 index 0000000000..eeb540f929 --- /dev/null +++ b/frontend/src/container/ListOfDashboard/DashboardTemplates/DashboardTemplatesModal.tsx @@ -0,0 +1,225 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import './DashboardTemplatesModal.styles.scss'; + +import { Button, Input, Modal, Typography } from 'antd'; +import ApacheIcon from 'assets/CustomIcons/ApacheIcon'; +import DockerIcon from 'assets/CustomIcons/DockerIcon'; +import ElasticSearchIcon from 'assets/CustomIcons/ElasticSearchIcon'; +import HerokuIcon from 'assets/CustomIcons/HerokuIcon'; +import KubernetesIcon from 'assets/CustomIcons/KubernetesIcon'; +import MongoDBIcon from 'assets/CustomIcons/MongoDBIcon'; +import MySQLIcon from 'assets/CustomIcons/MySQLIcon'; +import NginxIcon from 'assets/CustomIcons/NginxIcon'; +import PostgreSQLIcon from 'assets/CustomIcons/PostgreSQLIcon'; +import RedisIcon from 'assets/CustomIcons/RedisIcon'; +import cx from 'classnames'; +import { ConciergeBell, DraftingCompass, Drill, Plus, X } from 'lucide-react'; +import { ChangeEvent, useState } from 'react'; +import { DashboardTemplate } from 'types/api/dashboard/getAll'; + +import { filterTemplates } from '../utils'; + +const templatesList: DashboardTemplate[] = [ + { + name: 'Blank dashboard', + icon: , + id: 'blank', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, + { + name: 'Alert Manager', + icon: , + id: 'alertManager', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, + { + name: 'Apache', + icon: , + id: 'apache', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, + { + name: 'Docker', + icon: , + id: 'docker', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, + { + name: 'Elasticsearch', + icon: , + id: 'elasticSearch', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, + { + name: 'MongoDB', + icon: , + id: 'mongoDB', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, + { + name: 'Heroku', + icon: , + id: 'heroku', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, + { + name: 'Nginx', + icon: , + id: 'nginx', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, + { + name: 'Kubernetes', + icon: , + id: 'kubernetes', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, + { + name: 'MySQL', + icon: , + id: 'mySQL', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, + { + name: 'PostgreSQL', + icon: , + id: 'postgreSQL', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, + { + name: 'Redis', + icon: , + id: 'redis', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/redisTemplatePreview.svg', + }, + { + name: 'AWS', + icon: , + id: 'aws', + description: 'Create a custom dashboard from scratch.', + previewImage: '/Images/blankDashboardTemplatePreview.svg', + }, +]; + +interface DashboardTemplatesModalProps { + showNewDashboardTemplatesModal: boolean; + onCreateNewDashboard: () => void; + onCancel: () => void; +} + +export default function DashboardTemplatesModal({ + showNewDashboardTemplatesModal, + onCreateNewDashboard, + onCancel, +}: DashboardTemplatesModalProps): JSX.Element { + const [selectedDashboardTemplate, setSelectedDashboardTemplate] = useState( + templatesList[0], + ); + + const [dashboardTemplates, setDashboardTemplates] = useState(templatesList); + + const handleDashboardTemplateSearch = ( + event: ChangeEvent, + ) => { + const searchText = event.target.value; + const filteredTemplates = filterTemplates(searchText, templatesList); + setDashboardTemplates(filteredTemplates); + }; + + return ( + +
+
+ New Dashboard + + +
+ +
+
+ + +
+ {dashboardTemplates.map((template) => ( +
setSelectedDashboardTemplate(template)} + > +
{template.icon}
+
{template.name}
+
+ ))} +
+
+ +
+
+
+
+ {selectedDashboardTemplate.icon} +
+ +
+
{selectedDashboardTemplate.name}
+ +
+ {selectedDashboardTemplate.description} +
+
+
+ +
+ +
+
+ +
+ {`${selectedDashboardTemplate.name}-preview`} +
+
+
+
+
+ ); +} diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index a0a31c3142..f44b71e2bd 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -1,46 +1,91 @@ -import { PlusOutlined } from '@ant-design/icons'; -import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd'; -import { ItemType } from 'antd/es/menu/hooks/useItems'; +/* eslint-disable no-nested-ternary */ +/* eslint-disable jsx-a11y/img-redundant-alt */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +import './DashboardList.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { + Button, + Dropdown, + Flex, + Input, + MenuProps, + Modal, + Popover, + Skeleton, + Switch, + Table, + Tag, + Tooltip, + Typography, +} from 'antd'; +import { TableProps } from 'antd/lib'; import createDashboard from 'api/dashboard/create'; import { AxiosError } from 'axios'; +import cx from 'classnames'; +import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import { dashboardListMessage } from 'components/facingIssueBtn/util'; -import { - DynamicColumnsKey, - TableDataSource, -} from 'components/ResizeTable/contants'; -import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable'; -import LabelColumn from 'components/TableRenderer/LabelColumn'; -import TextToolTip from 'components/TextToolTip'; import { ENTITY_VERSION_V4 } from 'constants/app'; import ROUTES from 'constants/routes'; +import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils'; +import dayjs from 'dayjs'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; import useComponentPermission from 'hooks/useComponentPermission'; -import useDebouncedFn from 'hooks/useDebouncedFunction'; +import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import { Key, useCallback, useEffect, useMemo, useState } from 'react'; +import { get, isEmpty } from 'lodash-es'; +import { + ArrowDownWideNarrow, + ArrowUpRight, + CalendarClock, + Check, + Clock4, + Ellipsis, + EllipsisVertical, + Expand, + HdmiPort, + LayoutGrid, + Link2, + Plus, + Radius, + RotateCw, + Search, +} from 'lucide-react'; +import { handleContactSupport } from 'pages/Integrations/utils'; +import { + ChangeEvent, + Key, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { generatePath } from 'react-router-dom'; +import { useCopyToClipboard } from 'react-use'; import { AppState } from 'store/reducers'; import { Dashboard } from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; +import { isCloudUser } from 'utils/app'; -import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent'; -import useSortableTable from '../../hooks/ResizeTable/useSortableTable'; import useUrlQuery from '../../hooks/useUrlQuery'; -import { GettableAlert } from '../../types/api/alerts/get'; +import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal'; import ImportJSON from './ImportJSON'; -import { ButtonContainer, NewDashboardButton, TableContainer } from './styles'; -import DeleteButton from './TableComponents/DeleteButton'; -import Name from './TableComponents/Name'; -import { filterDashboard } from './utils'; - -const { Search } = Input; +import { DeleteButton } from './TableComponents/DeleteButton'; +import { + DashboardDynamicColumns, + DynamicColumns, + filterDashboard, +} from './utils'; +// eslint-disable-next-line sonarjs/cognitive-complexity function DashboardsList(): JSX.Element { const { data: dashboardListResponse = [], isLoading: isDashboardListLoading, + error: dashboardFetchError, refetch: refetchDashboardList, } = useGetAllDashboard(); @@ -51,6 +96,12 @@ function DashboardsList(): JSX.Element { role, ); + const [searchValue, setSearchValue] = useState(''); + const [ + showNewDashboardTemplatesModal, + setShowNewDashboardTemplatesModal, + ] = useState(false); + const { t } = useTranslation('dashboard'); const [ @@ -60,6 +111,9 @@ function DashboardsList(): JSX.Element { const [uploadedGrafana, setUploadedGrafana] = useState(false); const [isFilteringDashboards, setIsFilteringDashboards] = useState(false); + const [isConfigureMetadataOpen, setIsConfigureMetadata] = useState( + false, + ); const params = useUrlQuery(); const orderColumnParam = params.get('columnKey'); @@ -68,19 +122,57 @@ function DashboardsList(): JSX.Element { const searchParams = params.get('search'); const [searchString, setSearchString] = useState(searchParams || ''); - const [dashboards, setDashboards] = useState(); + const [sortOrder, setSortOrder] = useState({ + columnKey: orderColumnParam, + order: orderQueryParam, + pagination: paginationParam, + }); - const sortingOrder: 'ascend' | 'descend' | null = - orderQueryParam === 'ascend' || orderQueryParam === 'descend' - ? orderQueryParam - : null; + const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => { + const dashboardDynamicColumnsString = localStorage.getItem('dashboard'); + let dashboardDynamicColumns: DashboardDynamicColumns = { + createdAt: true, + createdBy: true, + updatedAt: false, + updatedBy: false, + }; + if (typeof dashboardDynamicColumnsString === 'string') { + try { + const tempDashboardDynamicColumns = JSON.parse( + dashboardDynamicColumnsString, + ); - const { sortedInfo, handleChange } = useSortableTable( - sortingOrder, - orderColumnParam || '', - searchString, + if (isEmpty(tempDashboardDynamicColumns)) { + localStorage.setItem('dashboard', JSON.stringify(dashboardDynamicColumns)); + } else { + dashboardDynamicColumns = { ...tempDashboardDynamicColumns }; + } + } catch (error) { + console.error(error); + } + } else { + localStorage.setItem('dashboard', JSON.stringify(dashboardDynamicColumns)); + } + + return dashboardDynamicColumns; + }; + + const [visibleColumns, setVisibleColumns] = useState( + () => getLocalStorageDynamicColumns(), ); + function setDynamicColumnsLocalStorage( + visibleColumns: DashboardDynamicColumns, + ): void { + try { + localStorage.setItem('dashboard', JSON.stringify(visibleColumns)); + } catch (error) { + console.error(error); + } + } + + const [dashboards, setDashboards] = useState(); + const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => { const sortedDashboards = dashboards.sort( (a, b) => @@ -89,6 +181,49 @@ function DashboardsList(): JSX.Element { setDashboards(sortedDashboards); }; + const sortDashboardsByUpdatedAt = (dashboards: Dashboard[]): void => { + const sortedDashboards = dashboards.sort( + (a, b) => + new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(), + ); + setDashboards(sortedDashboards); + }; + + useEffect(() => { + params.set('columnKey', sortOrder.columnKey as string); + params.set('order', sortOrder.order as string); + params.set('page', sortOrder.pagination || '1'); + history.replace({ search: params.toString() }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sortOrder]); + + const sortHandle = (key: string): void => { + console.log(dashboards); + if (!dashboards) return; + if (key === 'createdAt') { + sortDashboardsByCreatedAt(dashboards); + setSortOrder({ + columnKey: 'createdAt', + order: 'descend', + pagination: sortOrder.pagination || '1', + }); + } else if (key === 'updatedAt') { + sortDashboardsByUpdatedAt(dashboards); + setSortOrder({ + columnKey: 'updatedAt', + order: 'descend', + pagination: sortOrder.pagination || '1', + }); + } + }; + + function handlePageSizeUpdate(page: number): void { + setSortOrder((order) => ({ + ...order, + pagination: String(page), + })); + } + useEffect(() => { sortDashboardsByCreatedAt(dashboardListResponse); const filteredDashboards = filterDashboard( @@ -104,88 +239,6 @@ function DashboardsList(): JSX.Element { errorMessage: '', }); - const dynamicColumns: TableColumnProps[] = [ - { - title: 'Created At', - dataIndex: 'createdAt', - width: 30, - key: DynamicColumnsKey.CreatedAt, - sorter: (a: Data, b: Data): number => { - const prev = new Date(a.createdAt).getTime(); - const next = new Date(b.createdAt).getTime(); - - return prev - next; - }, - render: DateComponent, - sortOrder: - sortedInfo.columnKey === DynamicColumnsKey.CreatedAt - ? sortedInfo.order - : null, - }, - { - title: 'Created By', - dataIndex: 'createdBy', - width: 30, - key: DynamicColumnsKey.CreatedBy, - }, - { - title: 'Last Updated Time', - width: 30, - dataIndex: 'lastUpdatedTime', - key: DynamicColumnsKey.UpdatedAt, - sorter: (a: Data, b: Data): number => { - const prev = new Date(a.lastUpdatedTime).getTime(); - const next = new Date(b.lastUpdatedTime).getTime(); - - return prev - next; - }, - render: DateComponent, - sortOrder: - sortedInfo.columnKey === DynamicColumnsKey.UpdatedAt - ? sortedInfo.order - : null, - }, - { - title: 'Last Updated By', - dataIndex: 'lastUpdatedBy', - width: 30, - key: DynamicColumnsKey.UpdatedBy, - }, - ]; - - const columns = useMemo(() => { - const tableColumns: TableColumnProps[] = [ - { - title: 'Name', - dataIndex: 'name', - width: 40, - render: Name, - }, - { - title: 'Description', - width: 50, - dataIndex: 'description', - }, - { - title: 'Tags', - dataIndex: 'tags', - width: 50, - render: (value): JSX.Element => , - }, - ]; - - if (action) { - tableColumns.push({ - title: 'Action', - dataIndex: '', - width: 40, - render: DeleteButton, - }); - } - - return tableColumns; - }, [action]); - const data: Data[] = dashboards?.map((e) => ({ createdAt: e.created_at, @@ -198,6 +251,7 @@ function DashboardsList(): JSX.Element { createdBy: e.created_by, isLocked: !!e.isLocked || false, lastUpdatedBy: e.updated_by, + image: e.data.image || Base64Icons[0], refetchDashboardList, })) || []; @@ -238,167 +292,699 @@ function DashboardsList(): JSX.Element { } }, [newDashboardState, t]); - const getText = useCallback(() => { - if (!newDashboardState.error && !newDashboardState.loading) { - return 'New Dashboard'; - } - - if (newDashboardState.loading) { - return 'Loading'; - } - - return newDashboardState.errorMessage; - }, [ - newDashboardState.error, - newDashboardState.errorMessage, - newDashboardState.loading, - ]); - const onModalHandler = (uploadedGrafana: boolean): void => { setIsImportJSONModalVisible((state) => !state); setUploadedGrafana(uploadedGrafana); }; - const getMenuItems = useMemo(() => { - const menuItems: ItemType[] = [ - { - key: t('import_json').toString(), - label: t('import_json'), - onClick: (): void => onModalHandler(false), - }, - { - key: t('import_grafana_json').toString(), - label: t('import_grafana_json'), - onClick: (): void => onModalHandler(true), - disabled: true, - }, - ]; - - if (createNewDashboard) { - menuItems.unshift({ - key: t('create_dashboard').toString(), - label: t('create_dashboard'), - disabled: isDashboardListLoading, - onClick: onNewDashboardHandler, - }); - } - - return menuItems; - }, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]); - - const handleSearch = useDebouncedFn((event: unknown): void => { + const handleSearch = (event: ChangeEvent): void => { setIsFilteringDashboards(true); + setSearchValue(event.target.value); const searchText = (event as React.BaseSyntheticEvent)?.target?.value || ''; const filteredDashboards = filterDashboard(searchText, dashboardListResponse); setDashboards(filteredDashboards); setIsFilteringDashboards(false); setSearchString(searchText); - }, 500); + }; - const GetHeader = useMemo( - () => ( - -
- - + const [state, setCopy] = useCopyToClipboard(); - {createNewDashboard && ( - { + if (state.error) { + notifications.error({ + message: t('something_went_wrong', { + ns: 'common', + }), + }); + } + + if (state.value) { + notifications.success({ + message: t('success', { + ns: 'common', + }), + }); + } + }, [state.error, state.value, t, notifications]); + + function getFormattedTime(dashboard: Dashboard, option: string): string { + const timeOptions: Intl.DateTimeFormatOptions = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }; + const formattedTime = new Date(get(dashboard, option, '')).toLocaleTimeString( + 'en-US', + timeOptions, + ); + + const dateOptions: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + }; + + const formattedDate = new Date(get(dashboard, option, '')).toLocaleDateString( + 'en-US', + dateOptions, + ); + + // Combine time and date + return `${formattedDate} ⎯ ${formattedTime}`; + } + + const onLastUpdated = (time: string): string => { + const currentTime = dayjs(); + + const lastRefresh = dayjs(time); + + const secondsDiff = currentTime.diff(lastRefresh, 'seconds'); + + const minutedDiff = currentTime.diff(lastRefresh, 'minutes'); + const hoursDiff = currentTime.diff(lastRefresh, 'hours'); + const daysDiff = currentTime.diff(lastRefresh, 'days'); + const monthsDiff = currentTime.diff(lastRefresh, 'months'); + + if (isEmpty(time)) { + return `No updates yet!`; + } + + if (monthsDiff > 0) { + return `Last Updated ${monthsDiff} months ago`; + } + + if (daysDiff > 0) { + return `Last Updated ${daysDiff} days ago`; + } + + if (hoursDiff > 0) { + return `Last Updated ${hoursDiff} hrs ago`; + } + + if (minutedDiff > 0) { + return `Last Updated ${minutedDiff} mins ago`; + } + + return `Last Updated ${secondsDiff} sec ago`; + }; + + const columns: TableProps['columns'] = [ + { + title: 'Dashboards', + key: 'dashboard', + render: (dashboard: Data): JSX.Element => { + const timeOptions: Intl.DateTimeFormatOptions = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }; + const formattedTime = new Date(dashboard.createdAt).toLocaleTimeString( + 'en-US', + timeOptions, + ); + + const dateOptions: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + }; + + const formattedDate = new Date(dashboard.createdAt).toLocaleDateString( + 'en-US', + dateOptions, + ); + + // Combine time and date + const formattedDateAndTime = `${formattedDate} ⎯ ${formattedTime}`; + + const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`; + + const onClickHandler = (event: React.MouseEvent): void => { + if (event.metaKey || event.ctrlKey) { + window.open(getLink(), '_blank'); + } else { + history.push(getLink()); + } + }; + + return ( +
+
+
+ dashboard-image + {dashboard.name} +
+ +
+ {dashboard?.tags && dashboard.tags.length > 0 && ( +
+ {dashboard.tags.map((tag) => ( + + {tag} + + ))} +
+ )} + {action && ( + +
+ + +
+
+ +
+
+ } + placement="bottomRight" + arrow={false} + rootClassName="dashboard-actions" + > + { + e.stopPropagation(); + e.preventDefault(); + }} + /> + + )} +
+
+
+
+ + {formattedDateAndTime} +
+ + {dashboard.createdBy && ( +
+
+ + {dashboard.createdBy?.substring(0, 1).toUpperCase()} + +
+ + {dashboard.createdBy} + +
+ )} + {visibleColumns.updatedAt && ( +
+ + + {onLastUpdated(dashboard.lastUpdatedTime)} + +
+ )} + + {dashboard.lastUpdatedBy && visibleColumns.updatedBy && ( +
+ + Last Updated By -   + +
+ + {dashboard.lastUpdatedBy?.substring(0, 1).toUpperCase()} + +
+ + {dashboard.lastUpdatedBy} + +
+ )} +
+ + ); + }, + }, + ]; + + const getCreateDashboardItems = useMemo(() => { + const menuItems: MenuProps['items'] = [ + { + label: ( +
onModalHandler(false)} + > + Import JSON +
+ ), + key: '1', + }, + ]; + + if (createNewDashboard) { + menuItems.unshift({ + label: ( +
{ + onNewDashboardHandler(); }} > - - - + Create dashboard +
+ ), + key: '0', + }); + } - - } - type="primary" - data-testid="create-new-dashboard" - loading={newDashboardState.loading} - danger={newDashboardState.error} - > - {getText()} - - - - )} - - ), - [ - isDashboardListLoading, - handleSearch, - isFilteringDashboards, - searchString, - createNewDashboard, - getMenuItems, - newDashboardState.loading, - newDashboardState.error, - getText, - ], + return menuItems; + }, [createNewDashboard, onNewDashboardHandler]); + + const showPaginationItem = (total: number, range: number[]): JSX.Element => ( + <> + + {range[0]} — {range[1]} + + of {total} + ); return ( - - {GetHeader} +
+
+
+ Dashboards + + + Create and manage dashboards for your workspace. + + + +
- + {isDashboardListLoading || isFilteringDashboards ? ( +
+ + + + +
+ ) : dashboardFetchError ? ( +
+ something went wrong + + + Something went wrong :/ Please retry or contact support. + +
+ + + +
+
+ ) : dashboards?.length === 0 && !searchValue ? ( +
+ dashboards +
+ + No dashboards yet.{' '} + + + Create a dashboard to start visualizing your data + +
+ + {createNewDashboard && ( +
+ + + + + +
+ )} +
+ ) : ( + <> +
+ } + value={searchValue} + onChange={handleSearch} + /> + {createNewDashboard && ( + + + + )} +
+ + {dashboards?.length === 0 ? ( +
+ img + + No dashboards found for {searchValue}. Create a new dashboard? + +
+ ) : ( + <> +
+ + All Dashboards + +
+ + + + Sort By + + + +
+ } + rootClassName="sort-dashboards" + placement="bottomRight" + arrow={false} + > + + + + + +
+ } + rootClassName="configure-group" + placement="bottomRight" + arrow={false} + > + + + +
+ +
20 && { + pageSize: 20, + showTotal: showPaginationItem, + showSizeChanger: false, + onChange: (page): void => handlePageSizeUpdate(page), + defaultCurrent: Number(sortOrder.pagination) || 1, + } + } + /> + + )} + + )} onModalHandler(false)} /> - { + setShowNewDashboardTemplatesModal(false); }} /> - - + + { + setIsConfigureMetadata(false); + // reset to default if the changes are not applied + setVisibleColumns(getLocalStorageDynamicColumns()); + }} + title="Configure Metadata" + footer={ + + } + rootClassName="configure-metadata-root" + > +
+
+
+ dashboard-image + + {dashboards?.[0]?.data?.title} + +
+
+
+ {visibleColumns.createdAt && ( + + + {getFormattedTime(dashboards?.[0] as Dashboard, 'created_at')} + + )} + {visibleColumns.createdBy && ( +
+ + {dashboards?.[0]?.created_by?.substring(0, 1).toUpperCase()} + + + {dashboards?.[0]?.created_by} + +
+ )} +
+
+ {visibleColumns.updatedAt && ( + + + {onLastUpdated(dashboards?.[0]?.updated_at || '')} + + )} + {visibleColumns.updatedBy && ( +
+ + {dashboards?.[0]?.updated_by?.substring(0, 1).toUpperCase()} + + + {dashboards?.[0]?.updated_by} + +
+ )} +
+
+
+
+
+ + Created at +
+
+
+ + setVisibleColumns((prev) => ({ + ...prev, + [DynamicColumns.CREATED_AT]: check, + })) + } + /> +
+
+
+
+ + Created by +
+
+
+ + setVisibleColumns((prev) => ({ + ...prev, + [DynamicColumns.CREATED_BY]: check, + })) + } + /> +
+
+
+
+ + Updated at +
+
+
+ + setVisibleColumns((prev) => ({ + ...prev, + [DynamicColumns.UPDATED_AT]: check, + })) + } + /> +
+
+
+
+ + Updated by +
+
+
+ + setVisibleColumns((prev) => ({ + ...prev, + [DynamicColumns.UPDATED_BY]: check, + })) + } + /> +
+
+
+ +
+
); } @@ -413,6 +999,7 @@ export interface Data { lastUpdatedBy: string; isLocked: boolean; id: string; + image?: string; } export default DashboardsList; diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/importJSON.styles.scss b/frontend/src/container/ListOfDashboard/ImportJSON/importJSON.styles.scss new file mode 100644 index 0000000000..c099c19f80 --- /dev/null +++ b/frontend/src/container/ListOfDashboard/ImportJSON/importJSON.styles.scss @@ -0,0 +1,88 @@ +.import-json-modal { + .ant-modal-content { + border-radius: 4px; + border: 1px solid var(--bg-slate-400); + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ); + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px); + + padding: 0; + } + + .margin { + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ); + + backdrop-filter: blur(20px); + } + .view-lines { + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ); + + backdrop-filter: blur(20px); + } + + .import-json-content-header { + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--bg-slate-500); + } + + .import-json-modal-footer { + .create-dashboard-json-error { + margin-bottom: 8px; + display: flex; + } + + .action-btns-container { + display: flex; + justify-content: space-between; + } + } + + .ant-modal-footer { + margin-top: 0; + padding: 16px; + border-top: 1px solid var(--bg-slate-500); + } +} + +.lightMode { + .import-json-modal { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + + .margin { + background: var(--bg-vanilla-100); + } + .view-lines { + background: var(--bg-vanilla-100); + } + + .import-json-content-header { + border-bottom: 1px solid var(--bg-vanilla-300); + } + + .ant-modal-footer { + border-top: 1px solid var(--bg-vanilla-300); + + .ant-btn { + box-shadow: none; + } + } + } +} diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index 9db99c29e8..849305c7c2 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -1,20 +1,23 @@ +import './importJSON.styles.scss'; + import { red } from '@ant-design/colors'; import { ExclamationCircleTwoTone } from '@ant-design/icons'; +import MEditor, { Monaco } from '@monaco-editor/react'; +import { Color } from '@signozhq/design-tokens'; import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd'; import createDashboard from 'api/dashboard/create'; -import Editor from 'components/Editor'; import ROUTES from 'constants/routes'; +import { useIsDarkMode } from 'hooks/useDarkMode'; import { MESSAGE } from 'hooks/useFeatureFlag'; import { useNotifications } from 'hooks/useNotifications'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; import history from 'lib/history'; +import { MonitorDot, MoveRight, X } from 'lucide-react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath } from 'react-router-dom'; import { DashboardData } from 'types/api/dashboard/getAll'; -import { EditorContainer, FooterContainer } from './styles'; - function ImportJSON({ isImportJSONModalVisible, uploadedGrafana, @@ -125,62 +128,114 @@ function ImportJSON({ onModalHandler(); }; + const isDarkMode = useIsDarkMode(); + + function setEditorTheme(monaco: Monaco): void { + monaco.editor.defineTheme('my-theme', { + base: 'vs-dark', + inherit: true, + rules: [ + { token: 'string.key.json', foreground: Color.BG_VANILLA_400 }, + { token: 'string.value.json', foreground: Color.BG_ROBIN_400 }, + ], + colors: { + 'editor.background': Color.BG_INK_300, + }, + fontFamily: 'Space Mono', + fontSize: 20, + fontWeight: 'normal', + lineHeight: 18, + letterSpacing: -0.06, + }); + } + return ( - {t('import_json')} - {t('import_dashboard_by_pasting')} - - } + width="60vw" footer={ - - - {isCreateDashboardError && getErrorNode(t('error_loading_json'))} - {isFeatureAlert && ( - - {MESSAGE.CREATE_DASHBOARD} - +
+ {isCreateDashboardError && ( +
+ {getErrorNode(t('error_loading_json'))} +
)} - + + {isUploadJSONError && ( +
+ {getErrorNode(t('error_upload_json'))} +
+ )} + +
+ false} + action="none" + data={jsonData} + > + + + + + + {isFeatureAlert && ( + + {MESSAGE.CREATE_DASHBOARD} + + )} +
+
} > -
- - false} - action="none" - data={jsonData} - > - - - {isUploadJSONError && <>{getErrorNode(t('error_upload_json'))}} - +
+
+ {t('import_json')} - - {t('paste_json_below')} - setEditorValue(newValue)} - value={editorValue} - language="json" - /> - + +
+ + setEditorValue(newValue || '')} + value={editorValue} + options={{ + scrollbar: { + alwaysConsumeMouseWheel: false, + }, + minimap: { + enabled: false, + }, + fontSize: 14, + fontFamily: 'Space Mono', + }} + theme={isDarkMode ? 'my-theme' : 'light'} + // eslint-disable-next-line react/jsx-no-bind + beforeMount={setEditorTheme} + />
); diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/styles.ts b/frontend/src/container/ListOfDashboard/ImportJSON/styles.ts deleted file mode 100644 index 7dd936baef..0000000000 --- a/frontend/src/container/ListOfDashboard/ImportJSON/styles.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Space } from 'antd'; -import styled from 'styled-components'; - -export const EditorContainer = styled.div` - margin-top: 2rem; -`; - -export const FooterContainer = styled(Space)` - display: flex; -`; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.styles.scss b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.styles.scss index afd7a87d22..5a36e62502 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.styles.scss +++ b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.styles.scss @@ -3,3 +3,13 @@ align-items: center; } } + +.delete-btn:hover { + background-color: rgba(255, 255, 255, 0.12); +} + +.lightMode { + .delete-btn:hover { + background-color: rgba(0, 0, 0, 0.06) !important; + } +} diff --git a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx index 2791d365b8..288de55adc 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx @@ -3,8 +3,10 @@ import './DeleteButton.styles.scss'; import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; import { Modal, Tooltip, Typography } from 'antd'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import ROUTES from 'constants/routes'; import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard'; import { useNotifications } from 'hooks/useNotifications'; +import history from 'lib/history'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueryClient } from 'react-query'; @@ -21,13 +23,15 @@ interface DeleteButtonProps { name: string; id: string; isLocked: boolean; + routeToListPage?: boolean; } -function DeleteButton({ +export function DeleteButton({ createdBy, name, id, isLocked, + routeToListPage, }: DeleteButtonProps): JSX.Element { const [modal, contextHolder] = Modal.useModal(); const { role, user } = useSelector((state) => state.app); @@ -42,7 +46,7 @@ function DeleteButton({ const deleteDashboardMutation = useDeleteDashboard(id); const openConfirmationDialog = useCallback((): void => { - modal.confirm({ + const { destroy } = modal.confirm({ title: ( Are you sure you want to delete the @@ -51,24 +55,40 @@ function DeleteButton({ ), icon: , - onOk() { - deleteDashboardMutation.mutateAsync(undefined, { - onSuccess: () => { - notifications.success({ - message: t('dashboard:delete_dashboard_success', { - name, - }), - }); - queryClient.invalidateQueries([REACT_QUERY_KEY.GET_ALL_DASHBOARDS]); - }, - }); - }, okText: 'Delete', - okButtonProps: { danger: true }, + okButtonProps: { + danger: true, + onClick: (e) => { + e.preventDefault(); + e.stopPropagation(); + deleteDashboardMutation.mutateAsync(undefined, { + onSuccess: () => { + notifications.success({ + message: t('dashboard:delete_dashboard_success', { + name, + }), + }); + queryClient.invalidateQueries([REACT_QUERY_KEY.GET_ALL_DASHBOARDS]); + if (routeToListPage) { + history.replace(ROUTES.ALL_DASHBOARD); + } + destroy(); + }, + }); + }, + }, centered: true, className: 'delete-modal', }); - }, [modal, name, deleteDashboardMutation, notifications, t, queryClient]); + }, [ + modal, + name, + deleteDashboardMutation, + notifications, + t, + queryClient, + routeToListPage, + ]); const getDeleteTooltipContent = (): string => { if (isLocked) { @@ -87,14 +107,17 @@ function DeleteButton({ { + onClick={(e): void => { + e.preventDefault(); + e.stopPropagation(); if (!isLocked) { openConfirmationDialog(); } }} - disabled={isLocked} + className="delete-btn" + disabled={isLocked || (role === USER_ROLES.VIEWER && !isAuthor)} > - Delete + Delete dashboard @@ -103,6 +126,10 @@ function DeleteButton({ ); } +DeleteButton.defaultProps = { + routeToListPage: false, +}; + // This is to avoid the type collision function Wrapper(props: Data): JSX.Element { const { diff --git a/frontend/src/container/ListOfDashboard/utils.ts b/frontend/src/container/ListOfDashboard/utils.ts index ec3e1016d6..153e77c552 100644 --- a/frontend/src/container/ListOfDashboard/utils.ts +++ b/frontend/src/container/ListOfDashboard/utils.ts @@ -1,4 +1,4 @@ -import { Dashboard } from 'types/api/dashboard/getAll'; +import { Dashboard, DashboardTemplate } from 'types/api/dashboard/getAll'; export const filterDashboard = ( searchValue: string, @@ -25,3 +25,31 @@ export const filterDashboard = ( }); }); }; + +export const filterTemplates = ( + searchValue: string, + dashboardList: DashboardTemplate[], +): DashboardTemplate[] => { + const searchValueLowerCase = searchValue?.toLowerCase(); + + return dashboardList.filter((item: DashboardTemplate) => { + const { name } = item; + + // Check if any property value contains the searchValue + return name.toLowerCase().includes(searchValueLowerCase); + }); +}; + +export interface DashboardDynamicColumns { + createdAt: boolean; + createdBy: boolean; + updatedAt: boolean; + updatedBy: boolean; +} + +export enum DynamicColumns { + CREATED_AT = 'createdAt', + CREATED_BY = 'createdBy', + UPDATED_AT = 'updatedAt', + UPDATED_BY = 'updatedBy', +} diff --git a/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts b/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts index 5a0ef84741..b29b8d3ef6 100644 --- a/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts +++ b/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts @@ -84,7 +84,7 @@ export const useContextLogData = ({ const handleSuccess = useCallback( (data: SuccessResponse) => { - const currentData = data?.payload.data.newResult.data.result || []; + const currentData = data?.payload?.data?.newResult?.data?.result || []; if (currentData.length > 0 && currentData[0].list) { const currentLogs: ILog[] = currentData[0].list.map((item) => ({ diff --git a/frontend/src/container/LogsContextList/index.tsx b/frontend/src/container/LogsContextList/index.tsx index 1e3b885153..ca90640776 100644 --- a/frontend/src/container/LogsContextList/index.tsx +++ b/frontend/src/container/LogsContextList/index.tsx @@ -84,7 +84,7 @@ function LogsContextList({ const handleSuccess = useCallback( (data: SuccessResponse) => { - const currentData = data?.payload.data.newResult.data.result || []; + const currentData = data?.payload?.data?.newResult?.data?.result || []; if (currentData.length > 0 && currentData[0].list) { const currentLogs: ILog[] = currentData[0].list.map((item) => ({ diff --git a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss index 47cbf28738..a6142d195c 100644 --- a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss +++ b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss @@ -9,6 +9,7 @@ display: flex; flex-direction: column; flex: 1; + padding-bottom: 60px; .views-tabs-container { padding: 8px 16px; diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 622de9ba83..28e199066a 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -409,7 +409,7 @@ function LogsExplorerViews({ useEffect(() => { const currentParams = data?.params as Omit; - const currentData = data?.payload.data.newResult.data.result || []; + const currentData = data?.payload?.data?.newResult?.data?.result || []; if (currentData.length > 0 && currentData[0].list) { const currentLogs: ILog[] = currentData[0].list.map((item) => ({ ...item.data, @@ -650,7 +650,7 @@ function LogsExplorerViews({ {selectedPanelType === PANEL_TYPES.TABLE && ( diff --git a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx index 59f78499b3..f1d6e3642c 100644 --- a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx +++ b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx @@ -85,7 +85,7 @@ function LogsPanelComponent({ const [lastLog, setLastLog] = useState(); const { logs } = useLogsData({ - result: queryResponse.data?.payload.data.newResult.data.result, + result: queryResponse.data?.payload?.data?.newResult?.data?.result, panelType: PANEL_TYPES.LIST, stagedQuery: widget.query, }); diff --git a/frontend/src/container/LogsTopNav/index.tsx b/frontend/src/container/LogsTopNav/index.tsx index 40ce480e30..e80483484d 100644 --- a/frontend/src/container/LogsTopNav/index.tsx +++ b/frontend/src/container/LogsTopNav/index.tsx @@ -56,7 +56,7 @@ function LogsTopNav(): JSX.Element { : [], listQueryPayload: listQuery && listQuery[1] - ? listQuery[1].payload?.data.newResult.data.result || [] + ? listQuery[1].payload?.data?.newResult?.data?.result || [] : [], }; } diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx index 6b9ab4ee72..22224862a4 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx @@ -87,7 +87,7 @@ function TopOperationMetrics(): JSX.Element { }, ); - const queryTableData = data?.payload.data.newResult.data.result || []; + const queryTableData = data?.payload?.data?.newResult?.data?.result || []; const renderColumnCell = useMemo( () => diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/ComponentSlider.styles.scss b/frontend/src/container/NewDashboard/ComponentsSlider/ComponentSlider.styles.scss new file mode 100644 index 0000000000..48f2a83ec8 --- /dev/null +++ b/frontend/src/container/NewDashboard/ComponentsSlider/ComponentSlider.styles.scss @@ -0,0 +1,161 @@ +.graph-selection { + .ant-modal-content { + width: 515px; + max-height: 646px; + overflow-y: auto; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0px; + + .ant-modal-header { + height: 52px; + padding: 16px; + background: var(--bg-ink-400); + border-bottom: 1px solid var(--bg-slate-500); + margin-bottom: 0px; + + .ant-modal-title { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + } + } + + .ant-modal-body { + .panel-selection { + display: flex; + flex-flow: wrap; + padding: 16px; + gap: 16px; + + .selected { + background: var(--bg-slate-400); + } + + .ant-card { + display: flex; + height: 80px; + width: 232px; + padding: 19px 0px; + justify-content: center; + align-items: center; + border-radius: 4px; + cursor: pointer; + border: 1px solid var(--bg-slate-400); + + .ant-card-body { + padding: 0px; + border-radius: 0px; + display: flex; + flex-direction: column; + gap: 6px; + align-items: center; + + .ant-typography { + margin-top: 0px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + + .ant-card-body::before { + content: none; + } + .ant-card-body::after { + content: none; + } + } + } + } + + .ant-modal-footer { + border-radius: 0px 0px 4px 4px; + border-top: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + padding: 12px 15px; + margin-top: 0px; + + .ant-btn { + width: 100%; + display: flex; + align-items: center; + flex-direction: row-reverse; + justify-content: center; + color: var(--bg-vanilla-100); + + /* button/ small */ + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 200% */ + border-radius: 2px; + background: var(--bg-robin-500); + padding: 4px 8px; + gap: 4px; + } + } + + &::-webkit-scrollbar { + width: 0.1rem; + } + } +} + +.lightMode { + .graph-selection { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .ant-modal-header { + background: var(--bg-vanilla-100); + border-bottom: 1px solid var(--bg-vanilla-300); + + .ant-modal-title { + color: var(--bg-ink-300); + } + } + + .ant-modal-body { + .panel-selection { + .selected { + background: var(--bg-vanilla-200); + } + + .ant-card { + border: 1px solid var(--bg-vanilla-300); + + .ant-card-body { + .ant-typography { + color: var(--bg-ink-200); + } + } + } + } + } + + .ant-modal-footer { + border-top: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .ant-btn { + color: var(--bg-vanilla-100); + + background: var(--bg-robin-500); + } + } + } + } +} diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts b/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts index 467595702a..14ce24cfef 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts +++ b/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts @@ -11,6 +11,7 @@ export const PANEL_TYPES_INITIAL_QUERY = { [PANEL_TYPES.TRACE]: initialQueriesMap.traces, [PANEL_TYPES.BAR]: initialQueriesMap.metrics, [PANEL_TYPES.PIE]: initialQueriesMap.metrics, + [PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics, [PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics, }; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index a61255705a..2859b87305 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,3 +1,6 @@ +import './ComponentSlider.styles.scss'; + +import { Card, Modal } from 'antd'; import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; import createQueryParams from 'lib/createQueryParams'; @@ -8,10 +11,10 @@ import { v4 as uuid } from 'uuid'; import { PANEL_TYPES_INITIAL_QUERY } from './constants'; import menuItems from './menuItems'; -import { Card, Container, Text } from './styles'; +import { Text } from './styles'; function DashboardGraphSlider(): JSX.Element { - const { handleToggleDashboardSlider } = useDashboard(); + const { handleToggleDashboardSlider, isDashboardSliderOpen } = useDashboard(); // eslint-disable-next-line sonarjs/cognitive-complexity const onClickHandler = (name: PANEL_TYPES) => (): void => { @@ -56,15 +59,29 @@ function DashboardGraphSlider(): JSX.Element { } }; + const handleCardClick = (panelType: PANEL_TYPES): void => { + onClickHandler(panelType)(); + }; + return ( - - {menuItems.map(({ name, icon, display }) => ( - - {icon} - {display} - - ))} - + { + handleToggleDashboardSlider(false); + }} + rootClassName="graph-selection" + footer={null} + title="New Panel" + > +
+ {menuItems.map(({ name, icon, display }) => ( + handleCardClick(name)} id={name} key={name}> + {icon} + {display} + + ))} +
+
); } diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.tsx index 28493441a0..33977aa778 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.tsx @@ -12,34 +12,39 @@ import { const Items: ItemsProps[] = [ { name: PANEL_TYPES.TIME_SERIES, - icon: , + icon: , display: 'Time Series', }, { name: PANEL_TYPES.VALUE, - icon: , + icon: , display: 'Value', }, { name: PANEL_TYPES.TABLE, - icon:
, + icon:
, display: 'Table', }, { name: PANEL_TYPES.LIST, - icon: , + icon: , display: 'List', }, { name: PANEL_TYPES.BAR, - icon: , + icon: , display: 'Bar', }, { name: PANEL_TYPES.PIE, - icon: , + icon: , display: 'Pie', }, + { + name: PANEL_TYPES.HISTOGRAM, + icon: , + display: 'Histogram', + }, ]; export interface ItemsProps { diff --git a/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss b/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss index a5c677c679..78c5c05a4e 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss +++ b/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss @@ -1,13 +1,230 @@ -.dashboard-description-container { - box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25); +.settings-container-root { + .ant-drawer-wrapper-body { + border-left: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2); - border: 1px solid var(--bg-slate-400, #1d212d); - background: var(--bg-ink-400, #121317); - box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); - color: var(--bg-vanilla-400, #c0c1c3); + .ant-drawer-header { + height: 48px; + border-bottom: 1px solid var(--bg-slate-500); + padding: 14px 14px 14px 11px; + + .ant-drawer-header-title { + gap: 16px; + + .ant-drawer-title { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + padding-left: 16px; + border-left: 1px solid #161922; + } + + .ant-drawer-close { + height: 16px; + width: 16px; + margin-inline-end: 0px !important; + } + } + } + + .ant-drawer-body { + padding: 16px; + + &::-webkit-scrollbar { + width: 0.1rem; + } + } + } +} + +.dashboard-description-container { + box-shadow: none; + border: none; + background: unset; + box-shadow: none; + color: var(--bg-vanilla-400); .ant-card-body { - padding: 12px 16px; + padding: 0px; + } + + .dashboard-breadcrumbs { + height: 48px; + padding: 16px; + border-bottom: 1px solid var(--bg-slate-400); + display: flex; + gap: 6px; + align-items: center; + + .dashboard-btn { + display: flex; + align-items: center; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + padding: 0px; + height: 20px; + } + + .dashboard-btn:hover { + background-color: unset; + } + + .id-btn { + display: flex; + align-items: center; + padding: 0px 2px; + border-radius: 2px; + background: rgba(113, 144, 249, 0.1); + color: var(--bg-robin-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + height: 20px; + + .ant-btn-icon { + margin-inline-end: 4px; + } + } + .id-btn:hover { + background: rgba(113, 144, 249, 0.1); + color: var(--bg-robin-300); + } + } + + .dashbord-details { + display: flex; + justify-content: space-between; + .left-section { + display: flex; + padding: 10px 0px 0px 16px; + align-items: center; + gap: 8px; + + .dashboard-title { + color: #fff; + font-family: Inter; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 150% */ + letter-spacing: -0.08px; + flex-shrink: 0; + } + } + + .right-section { + display: flex; + align-items: center; + padding: 10px 16px 0px 0px; + gap: 14px; + + .icons { + display: flex; + align-items: center; + width: 32px; + height: 34px; + padding: 6px; + justify-content: center; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 10px; /* 83.333% */ + letter-spacing: 0.12px; + } + + .icons:hover { + background-color: unset; + } + .configure-button { + display: flex; + align-items: center; + width: 93px; + height: 34px; + padding: 6px; + justify-content: center; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 10px; /* 83.333% */ + letter-spacing: 0.12px; + } + + .add-panel-btn { + display: flex; + width: 119px; + height: 34px; + padding: 5.937px 11.875px; + justify-content: center; + align-items: center; + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 11.875px; + font-style: normal; + font-weight: 500; + line-height: 17.812px; /* 150% */ + } + } + } + + .dashboard-tags { + display: flex; + gap: 6px; + padding: 16px 16px 0px 16px; + .tag { + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + border-radius: 20px; + border: 1px solid rgba(173, 127, 88, 0.2); + background: rgba(173, 127, 88, 0.1); + color: var(--bg-sienna-400); + text-align: center; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + margin-inline-end: 0px; + } + } + .dashboard-description-section { + max-width: 957px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 22px; /* 157.143% */ + letter-spacing: -0.07px; + padding: 20px 16px 0px 16px; + } + + .dashboard-variables { + padding: 16px 16px 18px 16px; } } @@ -19,21 +236,467 @@ overflow: hidden; } -.dashboard-actions { - display: flex; - flex-direction: column; - gap: 8px; -} +.dashboard-settings { + width: 191px; + height: 302px; + flex-shrink: 0; -.lightMode { - .dashboard-description-container { - box-shadow: none; - border: 1px solid var(--bg-vanilla-300); - background-color: rgb(250, 250, 250); - color: var(--bg-ink-300); + .ant-popover-inner { + padding: 0px; + border-radius: 4px; + border: 1px solid var(--bg-slate-400); + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ) !important; + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px); + } - .ant-card-body { - padding: 12px 16px; + .menu-content { + display: flex; + flex-direction: column; + + .section-1 { + display: flex; + flex-direction: column; + align-items: start; + border-bottom: 1px solid #1d212d; + + .ant-btn { + display: flex; + width: 100%; + height: 20px; + padding: 16px 18px 18px 14px; + align-items: center; + gap: 6px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + .ant-btn-icon { + margin-inline-end: 0px; + } + } + } + .section-2 { + display: flex; + flex-direction: column; + align-items: start; + border-bottom: 1px solid #1d212d; + + .ant-btn { + display: flex; + width: 100%; + height: 20px; + padding: 16px 18px 18px 14px; + align-items: center; + gap: 6px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + } + .delete-dashboard { + display: flex; + flex-direction: column; + align-items: start; + + .ant-typography { + display: flex; + width: 100%; + height: 20px; + padding: 16px 18px 18px 14px; + align-items: center; + gap: 6px; + color: var(--bg-cherry-400) !important; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + } + } + } +} + +.rename-dashboard { + .ant-modal-content { + width: 384px; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0px; + + .ant-modal-header { + height: 52px; + padding: 16px; + background: var(--bg-ink-400); + border-bottom: 1px solid var(--bg-slate-500); + margin-bottom: 0px; + .ant-modal-title { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + width: 349px; + height: 20px; + } + } + + .ant-modal-body { + padding: 16px; + + .dashboard-content { + display: flex; + flex-direction: column; + gap: 8px; + + .name-text { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + } + + .dashboard-name-input { + display: flex; + padding: 6px 6px 6px 8px; + align-items: center; + gap: 4px; + align-self: stretch; + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + } + } + } + + .ant-modal-footer { + padding: 16px; + margin-top: 0px; + .dashboard-rename { + display: flex; + flex-direction: row-reverse; + gap: 12px; + + .cancel-btn { + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 2px; + background: var(--bg-slate-500); + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + + .rename-btn { + display: flex; + align-items: center; + display: flex; + width: 169px; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 2px; + background: var(--bg-robin-500); + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + } + } + } +} + +.section-naming { + .ant-modal-content { + width: 384px; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0px; + + .ant-modal-header { + height: 52px; + padding: 16px; + background: var(--bg-ink-400); + border-bottom: 1px solid var(--bg-slate-500); + margin-bottom: 0px; + .ant-modal-title { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + width: 349px; + height: 20px; + } + } + + .ant-modal-body { + padding: 16px; + + .section-naming-content { + display: flex; + flex-direction: column; + gap: 8px; + + .name-text { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + } + + .section-name-input { + display: flex; + width: 320px; + padding: 6px 6px 6px 8px; + align-items: center; + gap: 4px; + align-self: stretch; + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + } + } + } + + .ant-modal-footer { + padding: 16px; + margin-top: 0px; + .dashboard-rename { + display: flex; + flex-direction: row-reverse; + gap: 12px; + + .cancel-btn { + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 2px; + background: var(--bg-slate-500); + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + + .rename-btn { + display: flex; + align-items: center; + display: flex; + width: 140px; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 2px; + background: var(--bg-robin-500); + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + } + } + } +} + +.lightMode { + .settings-container-root { + .ant-drawer-wrapper-body { + border-left: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .ant-drawer-header { + border-bottom: 1px solid var(--bg-vanilla-300); + + .ant-drawer-header-title { + .ant-drawer-title { + color: var(--bg-ink-400); + border-left: 1px solid var(--bg-vanilla-300); + } + + .ant-drawer-close { + color: var(--bg-ink-300); + } + } + } + } + } + + .dashboard-description-container { + color: var(--bg-ink-400); + + .dashboard-breadcrumbs { + border-bottom: 1px solid var(--bg-vanilla-300); + + .dashboard-btn { + color: var(--bg-ink-400); + } + } + + .dashbord-details { + .left-section { + .dashboard-title { + color: var(--bg-ink-300); + } + } + + .right-section { + .icons { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + color: var(--bg-ink-400); + } + .configure-button { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + color: var(--bg-ink-400); + } + + .add-panel-btn { + color: var(--bg-vanilla-100); + } + } + } + + .dashboard-description-section { + color: var(--bg-ink-400); + } + } + .dashboard-settings { + .ant-popover-inner { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100) !important; + } + + .menu-content { + display: flex; + flex-direction: column; + + .section-1 { + border-bottom: 1px solid var(--bg-vanilla-300); + + .ant-btn { + color: var(--bg-ink-300); + } + } + .section-2 { + border-bottom: 1px solid var(--bg-vanilla-300); + + .ant-btn { + color: var(--bg-ink-300); + } + } + } + } + + .rename-dashboard { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .ant-modal-header { + background: var(--bg-vanilla-100); + border-bottom: 1px solid var(--bg-vanilla-300); + + .ant-modal-title { + color: var(--bg-ink-300); + } + } + + .ant-modal-body { + .dashboard-content { + .name-text { + color: var(--bg-ink-300); + } + + .dashboard-name-input { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + } + } + + .ant-modal-footer { + .dashboard-rename { + .cancel-btn { + background: var(--bg-vanilla-300); + } + } + } + } + } + + .section-naming { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .ant-modal-header { + background: var(--bg-vanilla-100); + border-bottom: 1px solid var(--bg-vanilla-300); + + .ant-modal-title { + color: var(--bg-ink-300); + } + } + + .ant-modal-body { + .section-naming-content { + .name-text { + color: var(--bg-ink-300); + } + + .section-name-input { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + } + } + + .ant-modal-footer { + .dashboard-rename { + .cancel-btn { + background: var(--bg-vanilla-300); + } + } + } } } } diff --git a/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx b/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx index c8a13acf0f..bbf0b12b45 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx @@ -1,6 +1,8 @@ -import { Button, Tooltip } from 'antd'; -import { Cog } from 'lucide-react'; -import { useState } from 'react'; +import './Description.styles.scss'; + +import { Button } from 'antd'; +import ConfigureIcon from 'assets/Integrations/ConfigureIcon'; +import { useRef, useState } from 'react'; import DashboardSettingsContent from '../DashboardSettings'; import { DrawerContainer } from './styles'; @@ -8,34 +10,38 @@ import { DrawerContainer } from './styles'; function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element { const [visible, setVisible] = useState(false); + const variableViewModeRef = useRef<() => void>(); + const showDrawer = (): void => { setVisible(true); }; const onClose = (): void => { setVisible(false); + variableViewModeRef?.current?.(); }; return ( <> - - - + ); diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index c916ec7501..0af57abdc7 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -1,25 +1,77 @@ import './Description.styles.scss'; -import { LockFilled, UnlockFilled } from '@ant-design/icons'; -import { Button, Card, Col, Row, Tag, Tooltip, Typography } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import { + Button, + Card, + Flex, + Input, + Modal, + Popover, + Tag, + Typography, +} from 'antd'; +import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; +import { dashboardHelpMessage } from 'components/facingIssueBtn/util'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; +import ROUTES from 'constants/routes'; +import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton'; +import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import useComponentPermission from 'hooks/useComponentPermission'; -import { Share2 } from 'lucide-react'; +import { useNotifications } from 'hooks/useNotifications'; +import history from 'lib/history'; +import { isEmpty } from 'lodash-es'; +import { + Check, + ClipboardCopy, + Ellipsis, + FileJson, + FolderKanban, + Fullscreen, + LayoutGrid, + LockKeyhole, + PenLine, + X, +} from 'lucide-react'; import { useDashboard } from 'providers/Dashboard/Dashboard'; -import { useState } from 'react'; +import { sortLayout } from 'providers/Dashboard/util'; +import { useCallback, useEffect, useState } from 'react'; +import { FullScreenHandle } from 'react-full-screen'; +import { Layout } from 'react-grid-layout'; +import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; +import { useCopyToClipboard } from 'react-use'; import { AppState } from 'store/reducers'; -import { DashboardData } from 'types/api/dashboard/getAll'; +import { Dashboard, DashboardData } from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; -import { USER_ROLES } from 'types/roles'; +import { ROLES, USER_ROLES } from 'types/roles'; +import { ComponentTypes } from 'utils/permission'; +import { v4 as uuid } from 'uuid'; +import DashboardGraphSlider from '../ComponentsSlider'; +import { Base64Icons } from '../DashboardSettings/General/utils'; import DashboardVariableSelection from '../DashboardVariablesSelection'; import SettingsDrawer from './SettingsDrawer'; -import ShareModal from './ShareModal'; +import { DEFAULT_ROW_NAME, downloadObjectAsJson } from './utils'; -function DashboardDescription(): JSX.Element { +interface DashboardDescriptionProps { + handle: FullScreenHandle; +} + +// eslint-disable-next-line sonarjs/cognitive-complexity +function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { + const { handle } = props; const { selectedDashboard, + panelMap, + setPanelMap, + layouts, + setLayouts, isDashboardLocked, + setSelectedDashboard, + handleToggleDashboardSlider, handleDashboardLockToggle, } = useDashboard(); @@ -30,12 +82,30 @@ function DashboardDescription(): JSX.Element { } : ({} as DashboardData); - const { title = '', tags, description } = selectedData || {}; + const { title = '', description, tags, image = Base64Icons[0] } = + selectedData || {}; - const [openDashboardJSON, setOpenDashboardJSON] = useState(false); + const [updatedTitle, setUpdatedTitle] = useState(title); - const { user, role } = useSelector((state) => state.app); + const [sectionName, setSectionName] = useState(DEFAULT_ROW_NAME); + + const updateDashboardMutation = useUpdateDashboard(); + + const { featureResponse, user, role } = useSelector( + (state) => state.app, + ); const [editDashboard] = useComponentPermission(['edit_dashboard'], role); + const [isDashboardSettingsOpen, setIsDashbordSettingsOpen] = useState( + false, + ); + + const [isRenameDashboardOpen, setIsRenameDashboardOpen] = useState( + false, + ); + + const [isPanelNameModalOpen, setIsPanelNameModalOpen] = useState( + false, + ); let isAuthor = false; @@ -43,91 +113,415 @@ function DashboardDescription(): JSX.Element { isAuthor = selectedDashboard?.created_by === user?.email; } - const onToggleHandler = (): void => { - setOpenDashboardJSON((state) => !state); - }; + let permissions: ComponentTypes[] = ['add_panel']; + + if (isDashboardLocked) { + permissions = ['add_panel_locked_dashboard']; + } + + const { notifications } = useNotifications(); + + const userRole: ROLES | null = + selectedDashboard?.created_by === user?.email + ? (USER_ROLES.AUTHOR as ROLES) + : role; + + const [addPanelPermission] = useComponentPermission(permissions, userRole); + + const onEmptyWidgetHandler = useCallback(() => { + handleToggleDashboardSlider(true); + }, [handleToggleDashboardSlider]); const handleLockDashboardToggle = (): void => { + setIsDashbordSettingsOpen(false); handleDashboardLockToggle(!isDashboardLocked); }; + const onNameChangeHandler = (): void => { + if (!selectedDashboard) { + return; + } + const updatedDashboard = { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + title: updatedTitle, + }, + }; + updateDashboardMutation.mutate(updatedDashboard, { + onSuccess: (updatedDashboard) => { + notifications.success({ + message: 'Dashboard renamed successfully', + }); + setIsRenameDashboardOpen(false); + if (updatedDashboard.payload) + setSelectedDashboard(updatedDashboard.payload); + }, + onError: () => { + notifications.error({ + message: SOMETHING_WENT_WRONG, + }); + setIsRenameDashboardOpen(true); + }, + }); + }; + + const [state, setCopy] = useCopyToClipboard(); + + const { t } = useTranslation(['dashboard', 'common']); + + useEffect(() => { + if (state.error) { + notifications.error({ + message: t('something_went_wrong', { + ns: 'common', + }), + }); + } + + if (state.value) { + notifications.success({ + message: t('success', { + ns: 'common', + }), + }); + } + }, [state.error, state.value, t, notifications]); + + function handleAddRow(): void { + if (!selectedDashboard) return; + const id = uuid(); + + const newRowWidgetMap: { widgets: Layout[]; collapsed: boolean } = { + widgets: [], + collapsed: false, + }; + const currentRowIdx = 0; + for (let j = currentRowIdx; j < layouts.length; j++) { + if (!panelMap[layouts[j].i]) { + newRowWidgetMap.widgets.push(layouts[j]); + } else { + break; + } + } + + const updatedDashboard: Dashboard = { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + layout: [ + { + i: id, + w: 12, + minW: 12, + minH: 1, + maxH: 1, + x: 0, + h: 1, + y: 0, + }, + ...layouts.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET), + ], + panelMap: { ...panelMap, [id]: newRowWidgetMap }, + widgets: [ + ...(selectedDashboard.data.widgets || []), + { + id, + title: sectionName, + description: '', + panelTypes: PANEL_GROUP_TYPES.ROW, + }, + ], + }, + uuid: selectedDashboard.uuid, + }; + + updateDashboardMutation.mutate(updatedDashboard, { + // eslint-disable-next-line sonarjs/no-identical-functions + onSuccess: (updatedDashboard) => { + if (updatedDashboard.payload) { + if (updatedDashboard.payload.data.layout) + setLayouts(sortLayout(updatedDashboard.payload.data.layout)); + setSelectedDashboard(updatedDashboard.payload); + setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); + } + + featureResponse.refetch(); + setIsPanelNameModalOpen(false); + setSectionName(DEFAULT_ROW_NAME); + }, + // eslint-disable-next-line sonarjs/no-identical-functions + onError: () => { + notifications.error({ + message: SOMETHING_WENT_WRONG, + }); + }, + }); + } + return ( - - - +
+
- - - - - - {selectedData && ( - - )} - -
- {!isDashboardLocked && editDashboard && ( - - )} - - - + + + + +
+
+ dashboard-img + {title} + {isDashboardLocked && } +
+
+ + setIsDashbordSettingsOpen(visible)} + rootClassName="dashboard-settings" + content={ +
+
+ {(isAuthor || role === USER_ROLES.ADMIN) && ( + + )} - {(isAuthor || role === USER_ROLES.ADMIN) && ( - - + )} + + +
+
+ {!isDashboardLocked && addPanelPermission && ( + + )} + + + +
+
+ +
+
+ } + trigger="click" + placement="bottomRight" + > + + )} +
+
+ {(tags?.length || 0) > 0 && ( +
+ {tags?.map((tag) => ( + + {tag} + + ))} +
+ )} + {!isEmpty(description) && ( +
{description}
+ )} + + {!isEmpty(selectedData.variables) && ( +
+ +
+ )} + + + { + // handle update dashboard here + }} + onCancel={(): void => { + setIsRenameDashboardOpen(false); + }} + rootClassName="rename-dashboard" + footer={ +
+ +
- - + } + > +
+ Enter a new name + setUpdatedTitle(e.target.value)} + /> +
+
+ handleAddRow()} + onCancel={(): void => { + setIsPanelNameModalOpen(false); + setSectionName(DEFAULT_ROW_NAME); + }} + footer={ +
+ + +
+ } + > +
+ Enter Section name + setSectionName(e.target.value)} + /> +
+
); } diff --git a/frontend/src/container/NewDashboard/DashboardDescription/utils.ts b/frontend/src/container/NewDashboard/DashboardDescription/utils.ts index c86698436d..80a2514632 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/utils.ts +++ b/frontend/src/container/NewDashboard/DashboardDescription/utils.ts @@ -12,3 +12,5 @@ export function downloadObjectAsJson( downloadAnchorNode.click(); downloadAnchorNode.remove(); } + +export const DEFAULT_ROW_NAME = 'Sample Row'; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettings.styles.scss b/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettings.styles.scss index d53e8c4070..399d6be533 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettings.styles.scss +++ b/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettings.styles.scss @@ -3,3 +3,134 @@ color: rgb(207, 19, 34); font-style: italic; } + +.dashboard-variable-settings-table { + .variable-name-drag { + display: flex; + align-items: center; + gap: 10px; + + .ant-table-cell { + padding: 0px !important; + border: none !important; + color: var(--bg-robin-400); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + } + + .variable-description-actions { + display: flex; + align-items: center; + gap: 10px; + flex: 1 0 0; + + .variable-description { + color: var(--bg-sienna-400); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + + .actions-btns { + position: absolute; + right: 10px; + display: none; + + &:hover { + display: inline-flex; + } + + .edit-variable-button { + width: 26px; + height: 22px; + border-radius: 2px; + display: flex; + padding: 4px 6px; + align-items: center; + gap: 3px; + background: var(--bg-slate-400); + } + + .delete-variable-button { + width: 26px; + height: 22px; + border-radius: 2px; + background: rgba(229, 72, 77, 0.1); + display: flex; + padding: 4px 6px; + align-items: center; + gap: 3px; + color: red; + } + } + } + + .ant-table-cell-row-hover { + .actions-btns { + display: inline-flex; + } + } + + .ant-table-thead { + .ant-table-cell { + padding: 8px 12px; + border: 1px solid var(--bg-slate-500); + background: unset; + } + + .ant-table-cell::before { + display: none; + } + } + + .ant-table-tbody { + .ant-table-cell { + padding: 14px; + border: 1px solid var(--bg-slate-500); + } + } + + .ant-table-row { + .ant-table-cell:nth-child(even) { + background: rgba(22, 25, 34, 0.4); + } + } +} + +.lightMode { + .dashboard-variable-settings-table { + .variable-description-actions { + .actions-btns { + .edit-variable-button { + background: var(--bg-vanilla-300); + } + } + } + + .ant-table-thead { + .ant-table-cell { + border: 1px solid var(--bg-vanilla-300); + } + } + + .ant-table-tbody { + .ant-table-cell { + border: 1px solid var(--bg-vanilla-300); + } + } + + .ant-table-row { + .ant-table-cell:nth-child(even) { + background: var(--bg-vanilla-200); + } + } + } +} diff --git a/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettingsContent.styles.scss b/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettingsContent.styles.scss new file mode 100644 index 0000000000..dac33f7ce1 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettingsContent.styles.scss @@ -0,0 +1,69 @@ +.settings-tabs { + .ant-tabs-nav-list { + width: 228px; + height: 32px; + flex-shrink: 0; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-400); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + transition: opacity 0.1s !important; + + .ant-tabs-tab + .ant-tabs-tab { + margin: 0px; + } + + .overview-btn { + width: 114px; + display: flex; + align-items: center; + justify-content: center; + } + + .variables-btn { + width: 114px; + display: flex; + align-items: center; + justify-content: center; + } + + .ant-tabs-ink-bar { + display: none; + } + + .ant-tabs-tab-active { + .overview-btn { + border-radius: 2px 0px 0px 2px; + background: var(--bg-slate-400); + } + + .variables-btn { + border-radius: 2px 0px 0px 2px; + background: var(--bg-slate-400); + } + } + } + + .ant-tabs-nav::before { + border-bottom: none; + } +} + +.lightMode { + .settings-tabs { + .ant-tabs-nav-list { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .ant-tabs-tab-active { + .overview-btn { + background: var(--bg-vanilla-300); + } + + .variables-btn { + background: var(--bg-vanilla-300); + } + } + } + } +} diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/AddTags.styles.scss b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/AddTags.styles.scss new file mode 100644 index 0000000000..2bd3d09988 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/AddTags.styles.scss @@ -0,0 +1,31 @@ +.tags-input { + display: flex; + border: none; + padding: 0px; + width: 183px; + height: 24px; + padding: 3px 1px; + flex-shrink: 0; +} + +.tag-container { + color: var(--bg-sienna-400); + font-family: 'Space Mono'; + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 153.846% */ + letter-spacing: 0.52px; + height: 24px; + flex-shrink: 0; + border-radius: 50px; + border: 1px solid rgba(173, 127, 88, 0.2); + background: rgba(173, 127, 88, 0.1); + padding: 2px 8px; +} + +.edit-input { + .ant-form-item { + margin-bottom: 0px; + } +} diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx index 82c016aba0..154e4a2ec4 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx @@ -1,5 +1,6 @@ -import { PlusOutlined } from '@ant-design/icons'; -import { Col, Tooltip, Typography } from 'antd'; +import './AddTags.styles.scss'; + +import { Col, Tooltip } from 'antd'; import Input from 'components/Input'; import { Dispatch, SetStateAction, useState } from 'react'; @@ -7,7 +8,6 @@ import { InputContainer, NewTagContainer, TagsContainer } from './styles'; function AddTags({ tags, setTags }: AddTagsProps): JSX.Element { const [inputValue, setInputValue] = useState(''); - const [inputVisible, setInputVisible] = useState(false); const [editInputIndex, setEditInputIndex] = useState(-1); const [editInputValue, setEditInputValue] = useState(''); @@ -15,7 +15,6 @@ function AddTags({ tags, setTags }: AddTagsProps): JSX.Element { if (inputValue) { setTags([...tags, inputValue]); } - setInputVisible(false); setInputValue(''); }; @@ -32,10 +31,6 @@ function AddTags({ tags, setTags }: AddTagsProps): JSX.Element { setTags(newTags); }; - const showInput = (): void => { - setInputVisible(true); - }; - const onChangeHandler = ( value: string, func: Dispatch>, @@ -48,7 +43,7 @@ function AddTags({ tags, setTags }: AddTagsProps): JSX.Element { {tags.map((tag, index) => { if (editInputIndex === index) { return ( -
+ 20; const tagElem = ( - handleClose(tag)}> + handleClose(tag)} + className="tag-container" + > { setEditInputIndex(index); @@ -87,32 +87,19 @@ function AddTags({ tags, setTags }: AddTagsProps): JSX.Element { ); })} - {inputVisible && ( - - - onChangeHandler(event.target.value, setInputValue) - } - onBlurHandler={handleInputConfirm} - onPressEnterHandler={handleInputConfirm} - /> - - )} - - {!inputVisible && ( - } onClick={showInput}> - - New Tag - - - )} + + + onChangeHandler(event.target.value, setInputValue) + } + onBlurHandler={handleInputConfirm} + onPressEnterHandler={handleInputConfirm} + /> + ); } diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/styles.ts b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/styles.ts index f2daefe65e..1fbc8a6974 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/styles.ts +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/styles.ts @@ -4,6 +4,8 @@ import styled from 'styled-components'; export const TagsContainer = styled.div` display: flex; align-items: center; + flex-flow: wrap; + gap: 6px; `; export const NewTagContainer = styled(Tag)` @@ -23,4 +25,6 @@ export const InputContainer = styled(Col)` > div { margin: 0; } + padding-left: 0px !important; + padding-right: 0px !important; `; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/GeneralSettings.styles.scss b/frontend/src/container/NewDashboard/DashboardSettings/General/GeneralSettings.styles.scss new file mode 100644 index 0000000000..47c6431721 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/GeneralSettings.styles.scss @@ -0,0 +1,197 @@ +.overview-content { + display: flex; + flex-direction: column; + + .overview-settings { + border-radius: 3px; + border: 1px solid var(--bg-slate-500); + padding: 16px !important; + + .name-icon-input { + display: flex; + .dashboard-image-input { + .ant-select-selector { + display: flex; + width: 32px; + height: 32px; + padding: 6px; + justify-content: center; + align-items: center; + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + + .ant-select-selection-item { + display: flex; + align-items: center; + + .list-item-image { + height: 16px; + width: 16px; + } + } + } + } + + .dashboard-name-input { + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + } + } + + .dashboard-name { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + } + + .description-text-area { + padding: 6px 6px 6px 8px; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + } + } + + .overview-settings-footer { + display: flex; + justify-content: space-between; + align-items: center; + width: -webkit-fill-available; + padding: 12px 16px 12px 0px; + position: fixed; + bottom: 0; + height: 32px; + border-top: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + + .unsaved { + display: flex; + align-items: center; + gap: 8px; + + .unsaved-dot { + width: 6px; + height: 6px; + border-radius: 50px; + background: var(--bg-robin-500); + box-shadow: 0px 0px 6px 0px rgba(78, 116, 248, 0.4); + } + .unsaved-changes { + color: var(--bg-robin-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ + letter-spacing: -0.07px; + } + } + .footer-action-btns { + display: flex; + gap: 8px; + + .discard-btn { + margin: '16px 0'; + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 24px; + } + + .save-btn { + margin: 0px !important; + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 24px; + } + } + } +} + +.dashboard-image-input { + &.ant-select-dropdown { + padding: 0px !important; + } + + .ant-select-item { + padding: 0px; + align-items: center; + justify-content: center; + + .ant-select-item-option-content { + display: flex; + align-items: center; + justify-content: center; + + .list-item-image { + height: 16px; + width: 16px; + } + } + } +} + +.lightMode { + .overview-content { + .overview-settings { + border: 1px solid var(--bg-vanilla-300); + + .name-icon-input { + .dashboard-image-input { + .ant-select-selector { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-300); + } + } + + .dashboard-name-input { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-300); + } + } + + .dashboard-name { + color: var(--bg-ink-400); + } + + .description-text-area { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + } + } + + .overview-settings-footer { + border-top: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .unsaved { + .unsaved-dot { + background: var(--bg-robin-500); + } + .unsaved-changes { + color: var(--bg-robin-400); + } + } + .footer-action-btns { + .discard-btn { + color: var(--bg-ink-300); + background-color: var(--bg-vanilla-300); + } + + .save-btn { + color: var(--bg-vanilla-300); + } + } + } + } +} diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx index 16c98bb54e..632cb13dae 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx @@ -1,14 +1,20 @@ -import { SaveOutlined } from '@ant-design/icons'; -import { Col, Input, Space, Typography } from 'antd'; +import './GeneralSettings.styles.scss'; + +import { Col, Input, Select, Space, Typography } from 'antd'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; +import { isEqual } from 'lodash-es'; +import { Check, X } from 'lucide-react'; import { useDashboard } from 'providers/Dashboard/Dashboard'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from './styles'; +import { Base64Icons } from './utils'; + +const { Option } = Select; function GeneralDashboardSettings(): JSX.Element { const { selectedDashboard, setSelectedDashboard } = useDashboard(); @@ -17,13 +23,18 @@ function GeneralDashboardSettings(): JSX.Element { const selectedData = selectedDashboard?.data; - const { title = '', tags = [], description = '' } = selectedData || {}; + const { title = '', tags = [], description = '', image = Base64Icons[0] } = + selectedData || {}; const [updatedTitle, setUpdatedTitle] = useState(title); const [updatedTags, setUpdatedTags] = useState(tags || []); const [updatedDescription, setUpdatedDescription] = useState( description || '', ); + const [updatedImage, setUpdatedImage] = useState(image); + const [numberOfUnsavedChanges, setNumberOfUnsavedChanges] = useState( + 0, + ); const { t } = useTranslation('common'); @@ -40,6 +51,7 @@ function GeneralDashboardSettings(): JSX.Element { description: updatedDescription, tags: updatedTags, title: updatedTitle, + image: updatedImage, }, }, { @@ -57,48 +69,135 @@ function GeneralDashboardSettings(): JSX.Element { ); }; - return ( - - -
- Name - setUpdatedTitle(e.target.value)} - /> -
+ useEffect(() => { + let numberOfUnsavedChanges = 0; + if (!isEqual(updatedTitle, selectedData?.title)) { + numberOfUnsavedChanges += 1; + } + if (!isEqual(updatedDescription, selectedData?.description)) { + numberOfUnsavedChanges += 1; + } + if (!isEqual(updatedTags, selectedData?.tags)) { + numberOfUnsavedChanges += 1; + } + if (!isEqual(updatedImage, selectedData?.image)) { + numberOfUnsavedChanges += 1; + } + setNumberOfUnsavedChanges(numberOfUnsavedChanges); + }, [ + selectedData?.description, + selectedData?.image, + selectedData?.tags, + selectedData?.title, + updatedDescription, + updatedImage, + updatedTags, + updatedTitle, + ]); -
- Description - setUpdatedDescription(e.target.value)} - /> + const discardHandler = (): void => { + setUpdatedTitle(title); + setUpdatedImage(image); + setUpdatedTags(tags); + setUpdatedDescription(description); + }; + + return ( +
+
+ +
+ + Dashboard Name + +
+ + setUpdatedTitle(e.target.value)} + /> +
+
+ +
+ + Description + + setUpdatedDescription(e.target.value)} + /> +
+
+ + Tags + + +
+
+ + {numberOfUnsavedChanges > 0 && ( +
+
+
+ + {numberOfUnsavedChanges} Unsaved change + +
+
+ + +
-
- Tags - -
-
- -
- - + )} +
); } diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/utils.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/utils.tsx new file mode 100644 index 0000000000..9f895c7acc --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/utils.tsx @@ -0,0 +1,22 @@ +export const Base64Icons = [ + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0Ljk0OTQgMTMuOTQyQzE2LjIzMTggMTIuNDI1OCAxNy4zMjY4IDkuNzAyMiAxNi4xOTU2IDYuNTc0ODdDMTUuNjQ0MyA1LjA1MjQ1IDE1LjAyMTkgNC4yMDI0OSAxNC4yOTY5IDMuNjYyNTJDMTMuODU1NyAzLjMzMzc5IDEyLjA5MzMgMi41MDYzMyA5Ljc1OTY1IDIuODY3NTZDOC4wNTM0OSAzLjEzMjU1IDUuNzc0ODcgNC4yMDg3NCA0LjI5MzY5IDUuOTU5OUMyLjg1NzUyIDcuNjYxMDYgMS43NDg4MyA5LjAwNDc0IDEuNjk3NTggMTAuMzA5N0MxLjYzMTMzIDExLjk4ODMgMi44OTYyNyAxMy40MzA4IDMuMDUwMDEgMTMuNjY0NUMzLjMyMzc0IDE0LjA3OTUgNS4xOTExNSAxNi40NTE4IDguNjk5NzEgMTYuNTczMUMxMS43OTcgMTYuNjc5MyAxMy44MTQ0IDE1LjI4NDQgMTQuOTQ5NCAxMy45NDJaIiBmaWxsPSIjNDAzRDNFIi8+CjxwYXRoIGQ9Ik00LjU1MzYzIDIuNzM3NDdDMi45Mzc0NiAzLjg5MTE2IDEuMTIxMzEgNi4yNTEwMyAxLjQ0NzU0IDkuNTYwODZDMS42MDYyOCAxMS4xNzIgMi4wMDI1MSAxMi4xNDk1IDIuNTcxMjMgMTIuODUwN0MyLjkxNzQ2IDEzLjI3ODIgNC40MTk4OCAxNC41NDkzIDYuNzczNTEgMTQuNzM2OEM5LjE0NTg4IDE0LjkyNTYgMTAuOTQ5NSAxNC4zOTQ0IDEyLjgzMzIgMTMuMDg0NEMxNi42NjE3IDEwLjQyMDggMTYuMDk4IDYuMzkzNTMgMTUuOTM0MyA1LjkyNDhDMTUuNzcwNSA1LjQ1NjA3IDE0LjU0NDQgMi42OTYyMiAxMS4xNzMzIDEuNzE1MDJDOC4xOTg0NCAwLjg1MDA2OCA1Ljk4MzU1IDEuNzE1MDIgNC41NTM2MyAyLjczNzQ3WiIgZmlsbD0iIzVFNjM2NyIvPgo8cGF0aCBkPSJNNy4zOTM1MyAyLjk2MTA5QzUuNjE3MzcgMi44OTczNCAzLjkxOTk2IDQuMjg4NTIgMy43NTYyMiA2LjAwNTkzQzMuNTkyNDggNy43MjIwOSA0LjY1NDkyIDkuMDI5NTIgNi4zMDk4MyA5LjI5NTc2QzcuOTY0NzUgOS41NjA3NCA5Ljg3ODM5IDguNTU1OCAxMC4yNjM0IDYuNDUwOTFDMTAuNjYwOSA0LjI4MjI3IDkuMDg5NjkgMy4wMjIzNCA3LjM5MzUzIDIuOTYxMDlaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNy45NDIxNyA1LjkwMTE1QzcuOTQyMTcgNS45MDExNSA4LjM2OTY1IDUuODEyNCA4LjQ1NDY1IDUuMTgyNDRDOC41MzgzOSA0LjU2MjQ3IDguMjMwOTEgNC4wMzM3NSA3LjUxMzQ1IDMuODQzNzZDNi43MzM0OSAzLjYzNzUyIDYuMjA0NzcgNC4wNjYyNSA2LjA2NzI3IDQuNTE3NDdDNS44NzYwMyA1LjE0NDk0IDYuMTU4NTIgNS40NDM2NyA2LjE1ODUyIDUuNDQzNjdDNi4xNTg1MiA1LjQ0MzY3IDUuMzkzNTYgNS42Mjc0MSA1LjMzMjMxIDYuNTI5ODdDNS4yNzQ4MSA3LjM4MTA3IDUuODU2MDMgNy44Mzg1NSA2LjQzOTc1IDcuOTc4NTRDNy4xNjA5NiA4LjE1MjI4IDcuOTc4NDIgNy45NTQ3OSA4LjE3ODQxIDcuMDM0ODRDOC4zNDQ2NSA2LjI3NzM4IDcuOTQyMTcgNS45MDExNSA3Ljk0MjE3IDUuOTAxMTVaIiBmaWxsPSIjMzAzMDMwIi8+CjxwYXRoIGQ9Ik02LjczOTgzIDQuNzUzNjJDNi42NzEwOSA1LjAxMjM1IDYuODA4NTggNS4yNjIzNCA3LjA3ODU3IDUuMzMxMDlDNy4zNjk4IDUuNDA0ODMgNy42MzQ3OSA1LjMwODU5IDcuNzA2MDMgNS4wMTExQzcuNzY4NTMgNC43NDczNyA3LjY0MzU0IDQuNTE0ODggNy4zMzYwNSA0LjQzOTg4QzcuMDgzNTcgNC4zNzczOSA2LjgxNDgzIDQuNDcxMTMgNi43Mzk4MyA0Ljc1MzYyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuOTU5NzggNi4wMzk3NEM2LjYzMjMgNS45Mzg0OSA2LjE5OTgyIDYuMDY0NzMgNi4xMzEwNyA2LjUwNDcxQzYuMDYyMzMgNi45NDQ2OSA2LjMyNjA2IDcuMTY5NjggNi42NzEwNCA3LjIzMjE3QzcuMDE2MDMgNy4yOTQ2NyA3LjM0MjI2IDcuMTEzNDMgNy40MDYwMSA2Ljc2MDk1QzcuNDY4NSA2LjQwOTcyIDcuMjg2MDEgNi4xMzk3MyA2Ljk1OTc4IDYuMDM5NzRaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1Ljc5OTEgMTUuMDM1NUMxNS43MzE3IDE0LjY3NTUgMTUuNDcxNyAxMi42ODQzIDE1LjQ3MTcgMTIuNjg0M0gyLjUyNzM1QzIuNTI3MzUgMTIuNjg0MyAyLjI2NzM2IDE0LjY3NTUgMi4xOTk4NyAxNS4wMzU1QzIuMTMyMzcgMTUuMzk1NCAyLjkwMzU4IDE2LjQ5OTEgOC45OTk1MSAxNi40OTkxQzE1LjA5NTQgMTYuNDk5MSAxNS44NjY2IDE1LjM5NTQgMTUuNzk5MSAxNS4wMzU1WiIgZmlsbD0iIzgyQUVDMCIvPgo8cGF0aCBkPSJNMi41MjM2OCAxMi43NDQ1QzIuNTIzNjggMTIuMDEwNyA1LjQyMzUzIDExLjQxNTggOC45OTk1OSAxMS40MTU4QzEyLjU3NTcgMTEuNDE1OCAxNS40NzU1IDEyLjAxMDcgMTUuNDc1NSAxMi43NDQ1QzE1LjQ3NTUgMTMuNDc4MiAxMi41NzU3IDE0LjE5NDQgOC45OTk1OSAxNC4xOTQ0QzUuNDIzNTMgMTQuMTk0NCAyLjUyMzY4IDEzLjQ3ODIgMi41MjM2OCAxMi43NDQ1WiIgZmlsbD0iI0UwRTBFMCIvPgo8cGF0aCBkPSJNMTMuMjIxNyAzLjAzOTkyQzEyLjg2NjcgMS45MDg3MyAxMS42ODQzIDEuNSA4Ljk5ODE2IDEuNUM2LjMxMjA1IDEuNSA1LjEzMDg3IDEuOTA4NzMgNC43NzU4OSAzLjAzOTkyQzQuMDYwOTIgNS4zMTg1NSAzLjQxODQ2IDEyLjUwMzIgMy40MTg0NiAxMi41MDMyQzMuNDE4NDYgMTIuNTAzMiAzLjk3NzE4IDEzLjQzOTQgOC45OTk0MSAxMy40Mzk0QzE0LjAyMTYgMTMuNDM5NCAxNC41ODE2IDEyLjUwNDQgMTQuNTgxNiAxMi41MDQ0QzE0LjU4MTYgMTIuNTA0NCAxMy45MzY3IDUuMzE4NTUgMTMuMjIxNyAzLjAzOTkyWiIgZmlsbD0iI0Y0NDMzNiIvPgo8cGF0aCBvcGFjaXR5PSIwLjkiIGQ9Ik0zLjQxNzI0IDEyLjUxN0MzLjQxNzI0IDExLjk0MjEgNS45MTU4NSAxMS40NzU4IDguOTk5NDQgMTEuNDc1OEMxMi4wODMgMTEuNDc1OCAxNC41ODE2IDExLjk0MjEgMTQuNTgxNiAxMi41MTdDMTQuNTgxNiAxMy4wOTIgMTIuMDgzIDEzLjY1MzIgOC45OTk0NCAxMy42NTMyQzUuOTE1ODUgMTMuNjUzMiAzLjQxNzI0IDEzLjA5MiAzLjQxNzI0IDEyLjUxN1oiIGZpbGw9IiNDNjI4MjgiLz4KPHBhdGggZD0iTTEwLjA3MzEgNS40NjU5NkwxMi44MDE3IDIuNjQ5ODZDMTIuODU0MiAyLjYwMzYxIDEyLjkzMTcgMi42NjIzNiAxMi45MDE3IDIuNzI0ODZMMTAuODcxOCA1Ljk4MjE4QzEwLjcyMDYgNi4yOTcxNyAxMC44NTQzIDYuNTAwOTEgMTEuMjA0MyA2LjQ5MjE2TDEzLjc4NTQgNi40NjU5MUMxMy44NTU0IDYuNDY0NjYgMTMuODc2NyA2LjU1OTY1IDEzLjgxMjkgNi41ODg0TDExLjQ1ODEgNy40OTU4NUMxMS4xMzkzIDcuNjM5NiAxMS4wNzE4IDcuOTgwODMgMTEuMzQ0MyA4LjE5ODMyTDE0LjI4MDQgMTAuMzE1N0MxNC4zMzU0IDEwLjM1OTUgMTQuMjkwNCAxMC40NDU3IDE0LjIyMjkgMTAuNDI3TDEwLjg4MTggOS4wNDMyN0MxMC41NDU2IDguOTQ4MjggMTAuMzkxOSA5LjEzNDUyIDEwLjQ2MDYgOS40NzdMMTEuNTM4MSAxMy4wODgxQzExLjU1MTggMTMuMTU2OCAxMS40NjE4IDEzLjE5NDMgMTEuNDIzMSAxMy4xMzY4TDkuNTg0NDEgOS44ODA3M0M5LjM4ODE3IDkuNTkxOTkgOS4xNTA2OCA5LjUzNyA4Ljk4NDQ0IDkuODQzMjNMNy4yMjA3OSAxMy4xNzgxQzcuMTg3MDQgMTMuMjM5MyA3LjA5NDU0IDEzLjIxMDYgNy4xMDA3OSAxMy4xNDA2TDguMDExOTkgOS41NDJDOC4wNDY5OSA5LjE5NDUyIDcuODg3IDkuMTIyMDIgNy41NjIwMiA5LjI0OTUxTDMuNzgwOTcgMTEuMTg4MkMzLjcxNTk3IDExLjIxMzIgMy42NjM0NyAxMS4xMzE5IDMuNzEzNDcgMTEuMDgzMkw2Ljg4NTggOC40NDk1NUM3LjEzNTc5IDguMjA1ODIgNy4xMjQ1NCA3LjkzOTU4IDYuNzkzMzEgNy44MjgzNEw0LjE0NTk1IDYuOTI1ODhDNC4wNzk3IDYuOTAzMzkgNC4wOTIyIDYuODA3MTQgNC4xNjA5NSA2LjgwMjE0TDYuNzY1ODEgNi44NTIxNEM3LjExNDU0IDYuODI1ODkgNy4yOTA3OCA2LjQ3ODQxIDcuMTA4MjkgNi4xODA5Mkw1LjAxNDY1IDMuMTYyMzNDNC45Nzg0IDMuMTAyMzQgNS4wNDk2NSAzLjAzNjA5IDUuMTA1OSAzLjA3NzM0TDcuODk4MjUgNS41MDIyMUM4LjE4MTk5IDUuNzA1OTUgOC40MzA3MiA1LjY4NzIgOC40ODE5NyA1LjM0MjIyTDguOTM1NyAyLjA5MjM5QzguOTQ1NyAyLjAyMzY0IDkuMDQzMTkgMi4wMTg2NCA5LjA2MDY5IDIuMDg2MTRMOS40NTU2NyA1LjI1ODQ3QzkuNTQzMTYgNS41OTcyIDkuODEwNjUgNS42OTcyIDEwLjA3MzEgNS40NjU5NloiIGZpbGw9InVybCgjcGFpbnQwX3JhZGlhbF83MTZfNTM1KSIvPgo8cGF0aCBkPSJNOS4yMzA2MyA1LjI1NzI0TDEwLjQwMDYgMy4xNDYxQzEwLjQzNTYgMy4wODYxMSAxMC41MjgxIDMuMTE3MzUgMTAuNTE5MyAzLjE4NjFMMTAuMTU0MyA1LjQ5MjIzQzEwLjExMTggNS44Mzg0NiAxMC4zMDQzIDUuOTg5NyAxMC42MzE4IDUuODY5NzFMMTIuOTIyOSA1LjA5OTc1QzEyLjk4NzkgNS4wNzYgMTMuMDM5MiA1LjE1ODUgMTIuOTg3OSA1LjIwNTk5TDExLjE5MyA2LjczODQxQzEwLjkzNjggNi45NzU5IDEwLjk4MTggNy4zMjA4OCAxMS4zMTA1IDcuNDM5NjNMMTMuNjAwNCA4LjIxNTg0QzEzLjY2NTQgOC4yMzk1OCAxMy42NTE2IDguMzM1ODMgMTMuNTgxNiA4LjMzOTU4TDExLjE0MTggOC4zODgzM0MxMC43OTMgOC40MDU4MyAxMC43MDY4IDguNjMyMDYgMTAuODgxOCA4LjkzNDU1TDEyLjExNDIgMTEuMDM5NEMxMi4xNDkyIDExLjA5OTQgMTIuMDc1NSAxMS4xNjQ0IDEyLjAyMDUgMTEuMTIxOUwxMC4xNzkzIDkuNTk3MDFDOS45MDA1OSA5LjM4NzAyIDkuNjU5MzYgOS40MDk1MiA5LjU5OTM2IDkuNzUzMjVMOS4xNjE4OCAxMi4yNDU2QzkuMTQ5MzggMTIuMzE0NCA5LjA1MTg5IDEyLjMxNjkgOS4wMzY4OSAxMi4yNDk0TDguNTgxOTEgOS43NzgyNUM4LjUwNDQyIDkuNDM4MjcgOC4zMjk0MyA5LjQyMDc3IDguMDYxOTQgOS42NDQ1MUw2LjEyNDU0IDExLjI3MzJDNi4wNzA4IDExLjMxODIgNS45OTQ1NSAxMS4yNTY5IDYuMDI3MDUgMTEuMTk1N0w3LjE2NTc0IDkuMTAzMjlDNy4zMjQ0OCA4Ljc5MjA2IDcuMjI5NDggOC41NDQ1NyA2Ljg3OTUgOC41NDQ1N0w0LjQyMjEzIDguNTc1ODJDNC4zNTIxNCA4LjU3NTgyIDQuMzMzMzkgOC40ODA4MiA0LjM5NzEzIDguNDUzMzJMNi41NDIwMiA3LjYyNzEyQzYuODYzMjUgNy40OTA4NyA2LjkxOTUgNy4xMDU4OSA2LjY1MjAxIDYuODgwOTFMNC44NTIxMSA1LjQxNDczQzQuNzk4MzYgNS4zNjk3NCA0Ljg0NTg2IDUuMjg0NzQgNC45MTIxMSA1LjMwNDc0TDcuMTgzMjQgNS45ODQ3QzcuNTE2OTcgNi4wODcyIDcuNzQ2OTYgNS45ODk3IDcuNjg1NzEgNS42NDU5N0w3LjIxNDQ4IDMuMjY5ODVDNy4yMDE5OSAzLjIwMTEgNy4yOTMyMyAzLjE2NjEgNy4zMzA3MyAzLjIyMzZMOC41ODE5MSA1LjI1NTk5QzguNzcwNjUgNS41NDk3MyA5LjA1Njg5IDUuNTU5NzMgOS4yMzA2MyA1LjI1NzI0WiIgZmlsbD0iI0ZGRDVDQSIvPgo8ZGVmcz4KPHJhZGlhbEdyYWRpZW50IGlkPSJwYWludDBfcmFkaWFsXzcxNl81MzUiIGN4PSIwIiBjeT0iMCIgcj0iMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJ0cmFuc2xhdGUoOC45OTk2MiA3LjYyNDY3KSBzY2FsZSg1LjQ0ODU0KSI+CjxzdG9wIG9mZnNldD0iMC41NzE4IiBzdG9wLWNvbG9yPSIjRkY2RTQwIi8+CjxzdG9wIG9mZnNldD0iMC43NjgyIiBzdG9wLWNvbG9yPSIjRkY3MDQ2IiBzdG9wLW9wYWNpdHk9IjAuNTQxNCIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGRjc1NTUiIHN0b3Atb3BhY2l0eT0iMCIvPgo8L3JhZGlhbEdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcuOTg0NCA1LjA1MTFDNy45ODQ0IDUuMDUxMSAzLjE3NDY2IDYuMzg4NTMgMi43OTk2OCA2LjkwNDc2QzIuNDI0NyA3LjQyMDk4IDEuNzc4NDggOC4zNzcxOCAxLjcxOTc0IDEwLjQyNDZDMS42NzIyNCAxMi4wNjcgMi41ODg0NCAxNi4yOTA1IDcuMDkxOTUgMTYuNDMwNUMxMS42OTA1IDE2LjU3NDIgMTMuNDAyOSAxMy41NDQ0IDE0LjIyNDEgMTEuNjY4M0MxNC45NjQgOS45NzU4NCAxNC45NTE1IDguMDMyMiAxNS40MjAzIDcuMTM5NzRDMTUuODg5IDYuMjQ3MjkgMTYuMzExNSA1LjQ3MzU4IDE2LjI4OSA0LjQ2NDg4QzE2LjI2NjUgMy40NTYxOSAxNS44OTAyIDIuNTg3NDggMTUuMjMyOCAyLjE2NTAxQzE0LjU3NTMgMS43NDI1MyAxNC4wMTI4IDEuODEyNTIgMTMuNzMxNiAxLjc0MjUzQzEzLjQ1MDQgMS42NzI1MyA3Ljk4NDQgNS4wNTExIDcuOTg0NCA1LjA1MTFaIiBmaWxsPSIjNzU3RTQwIi8+CjxwYXRoIGQ9Ik04LjgyODE3IDMuMzYyNDFDNi44MzMyOCA0LjE4MzYxIDQuMTgyMTcgNC45NTczMiAyLjkxNTk5IDYuNzQwOThDMS43NzIzIDguMzUwOSAxLjM5MTA3IDEwLjM1MzMgMi45Mzk3MyAxMi42OTk0QzQuMzUzNDEgMTQuODQxOCA4LjEyMTk2IDE1Ljc1NTUgMTAuOTQwNiAxMy43MzE5QzEzLjUyMTcgMTEuODc4MiAxMy43NTU0IDkuMTU3MSAxNC41MzA0IDcuMzQ5N0MxNS4zMDUzIDUuNTQyMjkgMTYuOTM1MiAzLjE2NzQyIDE0Ljk3NjYgMS45OTk5OEMxMi44ODggMC43NTc1NDUgMTAuNDA0MyAyLjcxMzY5IDguODI4MTcgMy4zNjI0MVoiIGZpbGw9IiNBRkI0MkEiLz4KPHBhdGggZD0iTTE0LjUzMDQgMi41MTczM0MxMy41ODE3IDEuOTY5ODYgMTIuMzcxNyAyLjIxMjM0IDExLjAzNDMgMi44Njk4MUM5LjY5Njg4IDMuNTI3MjggOC42NjQ0NCA0LjAxOTc1IDguMTk1NzEgNC4xODM0OUM3LjcyNTc0IDQuMzQ3MjMgMy43MzM0NSA1LjgwOTY2IDIuOTYzNDkgNy43NDk1NUMyLjMyOTc3IDkuMzQ0NDcgMi40OTQ3NiAxMS40MzMxIDQuMTU5NjcgMTIuOTU4QzUuODI0NTkgMTQuNDgyOSAxMC4wMjU2IDE0LjM4OTIgMTEuNjQ0MyAxMi4wODkzQzEzLjI2MjkgOS43ODk0NSAxMy4zMzI5IDcuOTM3MDQgMTMuNjg1NCA3LjExNTg0QzE0LjAzNzkgNi4yOTQ2MyAxNC44MDU0IDUuMDkyMTkgMTUuMDIyOSA0LjQ0MDk4QzE1LjI4MDMgMy42NjcyNyAxNS4xNDAzIDIuODY5ODEgMTQuNTMwNCAyLjUxNzMzWiIgZmlsbD0iI0ZGRjY5RCIvPgo8cGF0aCBkPSJNOS45MzA2NCA2Ljk3NDc1QzkuMTc5NDMgNi4wODM1NSA2LjMxNzA4IDYuMzY0NzggNS4zMzIxNCA3Ljc3MjIxQzQuMzQ3MTkgOS4xNzk2MyA0Ljc1NzE3IDEwLjMyMzMgNS4yMzgzOSAxMC45Mzk1QzUuODI0NjEgMTEuNjkwOCA3LjQyMDc4IDEyLjM3MDcgOC44NzQ0NSAxMS4zNjJDMTAuMzI4MSAxMC4zNTMzIDEwLjc1NDMgNy45NTIyIDkuOTMwNjQgNi45NzQ3NVoiIGZpbGw9IiM4NTVDNTIiLz4KPHBhdGggZD0iTTYuOTE0NDkgOC41NzcyM0M2LjU0NDUxIDkuMTE5NyA2LjcyOTUgOS45OTQ2NSA2LjA5MzI4IDEwLjAzNTlDNS40NTcwNyAxMC4wNzcyIDUuMDg3MDggOS4wNTU5NSA1LjUzODMxIDguMjAxQzYuMDI1NzkgNy4yNzg1NSA3LjE4MDcyIDYuODY3MzIgNy41MDk0NiA3LjMwNDhDNy44Mzk0NCA3Ljc0MzUyIDcuMjY1NzIgOC4wNjIyNiA2LjkxNDQ5IDguNTc3MjNaIiBmaWxsPSIjRDY3NjU5Ii8+Cjwvc3ZnPgo=', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyLjc3ODEgOS41NjgzNUMxMi43NzgxIDkuNTY4MzUgMTYuMjU0MiA4Ljg4NTg4IDE2LjQxOTIgMTAuMTk4M0MxNi41MTU0IDEwLjk3MiAxNi4yNzI5IDEyLjM5NDQgMTQuOTYzIDEzLjU2MzFDMTMuODc1NiAxNC41MzMxIDEyLjA1ODIgMTUuMzQ5MyA5LjA1OTU3IDE1LjI5NDNDNi42MTU5NSAxNS4yNTA1IDQuOTUzNTQgMTQuNjY0MyAzLjgyNDg1IDE0LjAxNTZDMS45MzQ5NSAxMi45Mjk0IDEuNTYxMjIgMTEuNDU3IDEuNjU5OTYgMTAuNTMzM0MxLjgwMTIxIDkuMjA4MzYgMy45MjQ4NCA4LjYwMDkgNC4wMjEwOSA4LjU0NTlMMTIuNzc4MSA5LjU2ODM1WiIgZmlsbD0iI0YxQjM0RiIvPgo8cGF0aCBkPSJNMS45MTUwNCAxMC44ODY5QzEuOTE1MDQgMTAuODg2OSAzLjkxOTkzIDE0LjIxNDIgOS4xMzcxNiAxNC4yMzE3QzE0Ljc4NTYgMTQuMjUxNyAxNi41MTE4IDEwLjg3ODEgMTYuNDE4IDEwLjE5NjlDMTYuMjQwNSA4Ljg5MDc1IDEzLjIzMDcgOC42MjQ1MSAxMy4yMzA3IDguNjI0NTFMNi4wNDg1NyAxMC4zMTY5TDEuOTE1MDQgOS44NjMyVjEwLjg4NjlaIiBmaWxsPSIjRkFERkIxIi8+CjxwYXRoIGQ9Ik0xLjkzNjE3IDkuNjgzNDNDMS45MzYxNyA5LjY4MzQzIDEuNjExMTkgMTAuMDczNCAxLjY2ODY4IDEwLjgxNzFDMS43MTk5MyAxMS40NzcxIDIuMzM5OSAxMi40NDk1IDMuNDIzNTkgMTIuODAwOEM0LjM4MjI5IDEzLjExMiA1LjA4NiAxMi43NTQ1IDYuMDc4NDUgMTMuMDc1N0M3LjA3MDkgMTMuMzk3IDcuOTg1ODUgMTQuMzcxOSA5LjI1MjAzIDE0LjI5NTdDMTAuNTE4MiAxNC4yMTk0IDExLjE5MDcgMTMuMzE0NSAxMi4xMDQ0IDEzLjE4MkMxMy40NzY4IDEyLjk4MzMgMTQuMTMzIDEzLjM1MiAxNS4xNzA1IDEyLjMyODNDMTYuMzE0MiAxMS4xOTk2IDE2LjMyMDQgMTAuMTQ4NCAxNi4zMTQyIDkuODg3MTZDMTYuMzAxNyA5LjM1OTY5IDE1LjkyNzkgOS4wNzQ3MSAxNS45Mjc5IDkuMDc0NzFMMy4zMzExIDkuNTM1OTNMMS45MzYxNyA5LjY4MzQzWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTkuMDc5NTcgMi42ODQ4MUM0LjEyNDgzIDIuNjg0ODEgMS42NDI0NiA1LjI3NTkzIDEuNDQ3NDcgOC4yMzA3N0MxLjM3MjQ4IDkuMzY4MjEgMS43NDI0NiAxMC4xMTMyIDIuNTQ4NjcgMTAuODIzMUMzLjk1NjA5IDEyLjA2MDYgNi41MjQ3MSAxMi44MzU1IDkuMTc4MzIgMTIuODY0M0MxMi4wMjE5IDEyLjg5NTUgMTQuMTc5MyAxMi4xNzU2IDE1LjgzMDUgOS45OTY5M0MxNi4zNjkyIDkuMjg2OTcgMTYuNDI3OSA4Ljg4MzI0IDE2LjQ1MDQgOC4wOTMyOEMxNi41MTkyIDUuNjE1OTEgMTQuMjkwNSAyLjY4NDgxIDkuMDc5NTcgMi42ODQ4MVoiIGZpbGw9InVybCgjcGFpbnQwX3JhZGlhbF83MjFfMTQ1KSIvPgo8cGF0aCBkPSJNNi41Mzg0MiA3LjI0NDU5QzYuNDgyMTcgNy4xMjA4NCA2LjkxNzE1IDYuOTY5NiA3LjEzMzM5IDYuODEyMTFDNy4zNzA4OCA2LjYzODM3IDcuNDE0NjIgNi40MzMzOCA3LjUwMDg3IDYuNDIyMTNDNy41ODcxMSA2LjQxMDg4IDcuOTY1ODQgNi41MjgzNyA4LjkzOTU0IDYuNTQwODdDOS43OTQ1IDYuNTUyMTIgMTAuMzM1NyA2LjMyNDYzIDEwLjQxMDcgNi4zODk2M0MxMC40ODcgNi40NTQ2MyAxMC41OTQ1IDYuNjM4MzcgMTAuODMzMiA2Ljg3NzExQzExLjAzOTQgNy4wODMzNCAxMS4yNzY5IDcuMjIzMzQgMTEuMjg4MiA3LjI5OTU4QzExLjI5OTQgNy4zNzU4MyAxMC43NTE5IDcuNDkyMDcgMTAuNjU1NyA3LjUxMzMyQzEwLjU1ODIgNy41MzQ1NyA3LjQzNDYyIDcuNTI1ODIgNy40MzQ2MiA3LjUyNTgyQzcuNDM0NjIgNy41MjU4MiA3LjI1MjEzIDcuNDcyMDcgNy4xMDA4OSA3LjQyOTU4QzYuOTQ5NjUgNy4zODQ1OCA2LjU5MjE3IDcuMzYzMzMgNi41Mzg0MiA3LjI0NDU5WiIgZmlsbD0iI0ZDREU4QyIvPgo8cGF0aCBkPSJNOC45NzIxMyA3Ljk5MDkxQzkuNTIzMzUgNy45OTA5MSA5LjgyNDU4IDcuODI5NjcgMTAuMTk0NiA3LjY5ODQyQzEwLjU4MDggNy41NjIxOCAxMC44OTgzIDcuNDQ5NjkgMTAuODk4MyA3LjQ0OTY5QzEwLjg5ODMgNy40NDk2OSAxMC4zMjQ2IDYuODY1OTcgOS4wNTgzNyA2Ljg2NTk3QzcuNzkyMTkgNi44NjU5NyA3LjI0MDk3IDcuNDYwOTQgNy4yNDA5NyA3LjQ2MDk0QzcuMjQwOTcgNy40NjA5NCA4LjMyMzQxIDcuOTkwOTEgOC45NzIxMyA3Ljk5MDkxWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTUuNjcyMTUgNC42MjQ5NEM1LjUyMzQxIDQuNjU4NjkgNS41MTg0MSA0Ljg4NjE4IDUuNDczNDEgNS4xMDExN0M1LjM4MjE3IDUuNTMzNjUgNS41ODcxNiA1Ljg4MzYzIDUuOTc1ODkgNS43NzIzOEM2LjQyMDg2IDUuNjQ0ODkgNi4zMTk2MiA1LjIwODY2IDYuMDU4MzggNC45MTg2OEM1LjkwNTg5IDQuNzUxMTkgNS44MjQ2NCA0LjU4OTk0IDUuNjcyMTUgNC42MjQ5NFoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTcuNzU5NjUgNC4zMTg2MkM3LjcxOTY1IDQuNDI2MTEgNy4zNjg0MiA0LjQ5OTg2IDcuMDEwOTQgNC40NTk4NkM2Ljc0NTk1IDQuNDI5ODYgNi41MjM0NiA0LjE2NDg4IDYuNjY0NyAzLjg2OTg5QzYuODA4NDUgMy41NzExNiA3LjE2NTkzIDMuNTM2MTYgNy40MDg0MSAzLjc3MzY1QzcuNjU3MTUgNC4wMTczOCA3LjgwMzM5IDQuMTk5ODcgNy43NTk2NSA0LjMxODYyWiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNOC41MjU3NiA0LjEyNDkzQzguNDQwNzcgNC4xMDc0MyA4LjE2MjAzIDQuMzQ2MTcgOC4xMDA3OSA0LjYxMjRDOC4wMzA3OSA0LjkwOTg5IDguMjMwNzggNS4wMzIzOCA4LjM5NTc3IDUuMDQ5ODhDOC41OTk1MSA1LjA3MTEzIDguNzQ3IDQuOTEzNjQgOC43NTk1IDQuNzA5OUM4Ljc2OTUgNC41MDc0MSA4LjYxNzAxIDQuMTQzNjggOC41MjU3NiA0LjEyNDkzWiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNNC4yMDczNyA3LjI1NzExQzQuMjY2MTEgNy4zOTgzNSA0LjQ5MTEgNy4zNjIxIDQuNzExMDkgNy4zNjgzNUM1LjE1MjMyIDcuMzgwODUgNS40NjEwNSA3LjExODM2IDUuMjgzNTYgNi43NTQ2M0M1LjA3OTgyIDYuMzM4NCA0LjY2NzM0IDYuNTE3MTUgNC40Mjk4NSA2LjgyNDYzQzQuMjkxMTEgNy4wMDQ2MiA0LjE0NzM3IDcuMTEzMzYgNC4yMDczNyA3LjI1NzExWiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMi42Nzc0IDcuMjM2MDNDMi41MjQ5MSA3LjIyODUzIDIuNDYxMTYgNy40NDcyNyAyLjM2MTE3IDcuNjQzNTFDMi4xNTk5MyA4LjAzNTk5IDIuMjY0OTIgOC40Mjg0NiAyLjY2ODY1IDguNDIzNDdDMy4xMzExMyA4LjQxODQ3IDMuMTQ3MzggNy45Njk3NCAyLjk3MjM5IDcuNjIyMjZDMi44Njk4OSA3LjQxOTc3IDIuODMzNjQgNy4yNDIyOCAyLjY3NzQgNy4yMzYwM1oiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTE0LjAxMTcgNi45Mjk2NUMxNC4xNjMgNi45MTA5IDE0LjE4OTIgNi42ODQ2NiAxNC4yNTY3IDYuNDc0NjdDMTQuMzg5MiA2LjA1MzQ0IDE0LjIyMDUgNS42ODQ3MSAxMy44MjMgNS43NTcyMUMxMy4zNjY4IDUuODM5NyAxMy40MjU1IDYuMjg0NjggMTMuNjU2NyA2LjU5ODQxQzEzLjc5MTcgNi43ODA5IDEzLjg1NjcgNi45NDg0IDE0LjAxMTcgNi45Mjk2NVoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTUuNzkzNDYgOS4yMTA3NEM1LjcyMDk2IDkuMTU4MjQgNS40OTA5NyA5LjIyNjk4IDUuMjg1OTggOS4zMDgyM0M0Ljg3NiA5LjQ3MDcyIDQuNjk2MDEgOS44MzQ0NSA1LjAwMjI1IDEwLjA5NjlDNS4zNTM0OCAxMC4zOTgyIDUuNjYyMjEgMTAuMDcxOSA1Ljc1OTcxIDkuNjk1NzFDNS44MTcyIDkuNDc2OTcgNS44ODA5NSA5LjI3NDQ4IDUuNzkzNDYgOS4yMTA3NFoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTUuMDcyMzggOC43OTIxNEM1LjEwNzM4IDguNzIyMTQgNS4wMTM2NCA4LjUzMjE1IDQuOTE5ODkgOC4zODA5MUM0LjgxMTE1IDguMjA0NjcgNC42ODc0IDguMDI0NjggNC41MTQ5MSA3Ljk3NTkzQzMuOTU5OTQgNy44MTk2OSAzLjc1OTk1IDguNTI3MTUgNC4xMzI0MyA4Ljc1MzM5QzQuMjY4NjggOC44MzU4OCA0LjQxMjQyIDguODQwODggNC41NzExNiA4Ljg0OTYzQzQuODA4NjUgOC44NTk2MyA1LjAzODY0IDguODU5NjMgNS4wNzIzOCA4Ljc5MjE0WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMy44MTExNyA2Ljc1MzM5QzMuNzg4NjcgNi44OTQ2MyAzLjE2OTk2IDcuMDEyMTIgMi44NTM3MiA2LjgyMDg4QzIuNDc0OTkgNi41OTA5IDIuNjc4NzMgNi4wNjA5MiAzLjEyOTk2IDYuMDk5NjdDMy41MTYxOSA2LjEzNDY3IDMuODM2MTcgNi41OTM0IDMuODExMTcgNi43NTMzOVoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTQuMTMxMDUgNS45ODIxNkM0LjI3NzI5IDYuMDIyMTYgNC42OTk3NyA1LjcwNTkzIDQuODIzNTEgNS40NzU5NEM0LjkxMzUxIDUuMzA4NDUgNC45ODYgNC44OTIyMiA0LjY5MzUyIDQuNzYwOThDNC4zNjcyOSA0LjYxNDc0IDQuMDg4NTUgNC44Mjk3MiA0LjAwNjA2IDUuMDk4NDZDMy45MDEwNiA1LjQ1MzQ0IDQuMDAxMDYgNS45NDcxNyA0LjEzMTA1IDUuOTgyMTZaIiBmaWxsPSIjRjVFNUM3Ii8+CjxwYXRoIGQ9Ik02LjYzMSA3LjcwNTg3QzYuNjU0NzUgNy44MTQ2MiA2LjQ0NDc2IDguNDA5NTkgNS45NTQ3OCA4LjQzMjA4QzUuNjE3MyA4LjQ0NzA4IDUuMzI0ODIgNy45OTMzNiA1Ljc0NjA0IDcuNjk5NjJDNi4wNjYwMyA3LjQ3ODM4IDYuNjE0NzUgNy42MjcxMyA2LjYzMSA3LjcwNTg3WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNNy4wNTM0NCA4LjY1NDY4QzYuOTU3MiA4LjU3MzQzIDYuNDUwOTggOC45MzU5MSA2LjI5ODQ4IDkuMjAwOUM2LjIwODQ5IDkuMzU4MzkgNi4xNDU5OSA5LjY0NTg4IDYuNTE4NDcgOS44MTQ2MkM2Ljg3ODQ1IDkuOTc4MzYgNy4wNzU5NCA5LjcwMjEyIDcuMTIwOTQgOS40NzcxM0M3LjE2NTk0IDkuMjUyMTUgNy4xMjcxOSA4LjcxNzE3IDcuMDUzNDQgOC42NTQ2OFoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTcuNTYwODggMTAuNDQyMUM3LjY4NDYyIDEwLjY3MzMgOC4zMjU4NCAxMC41NzMzIDguNDc4MzMgMTAuNTI3MUM4LjY5ODMyIDEwLjQ1OTYgOS4wMDk1NSAxMC4yNTQ2IDguODEwODEgOS44MjgzNUM4LjY0NzA3IDkuNDc5NjIgOC4yMTk1OSA5LjU1MjEyIDcuOTYwODUgOS43NzIxQzcuNzM3MTIgOS45NjA4NCA3LjQ3NTg4IDEwLjI4NDYgNy41NjA4OCAxMC40NDIxWiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNOS42MjcxMiA5LjYyNTc2QzkuNDk0NjMgOS43MjMyNiA5LjU5OTYzIDEwLjMxOTUgOS42NzgzNyAxMC40OTgyQzkuNzc5NjIgMTAuNzI5NSAxMC4xMDU4IDEwLjgxOTUgMTAuMzI1OCAxMC42NTA3QzEwLjU3OTYgMTAuNDU1NyAxMC41Mzk2IDEwLjEzMzIgMTAuMzc3MSA5Ljk1ODI1QzEwLjI1MzMgOS44MjcgOS43NTU4NyA5LjUyOTUyIDkuNjI3MTIgOS42MjU3NloiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTEwLjU3ODIgOS4yNTk1MkMxMC41Mjk1IDkuMzAzMjcgMTAuMDk0NSA5LjMzMzI2IDkuNzg0NTEgOS4wNjgyOEM5LjYwNTc3IDguOTE1NzkgOS42MTA3NyA4LjU1NDU1IDkuNzkwNzYgOC40MjA4MUM5Ljk4ODI1IDguMjc0NTcgMTAuMjU4MiA4LjMxOTU3IDEwLjQzMiA4LjU3MzNDMTAuNTQwNyA4LjcyOTU1IDEwLjYzNDUgOS4yMDgyNyAxMC41NzgyIDkuMjU5NTJaIiBmaWxsPSIjRjVFNUM3Ii8+CjxwYXRoIGQ9Ik0xMS4yMzY5IDkuMDQ1NzNDMTEuMzI0NCA5LjEyMTk4IDExLjg3ODEgOS4wMjgyMyAxMi4wODY4IDguNzg3QzEyLjIwMDYgOC42NTQ1IDEyLjM2MzEgOC4zNzA3NyAxMi4xNDMxIDguMTY4MjhDMTEuOTIzMSA3Ljk2NTc5IDExLjY2NDMgOC4wMDQ1NCAxMS40Mzk0IDguMjgwNzdDMTEuMjcxOSA4LjQ4NDUxIDExLjE0NjkgOC45NjY5OSAxMS4yMzY5IDkuMDQ1NzNaIiBmaWxsPSIjRjVFNUM3Ii8+CjxwYXRoIGQ9Ik0xMy4zODE5IDcuMzg0OEMxMy4zMzk0IDcuNTU0NzkgMTIuNzE4MiA3LjYyNDc5IDEyLjQ2OTUgNy41MDg1NEMxMi4yMTU4IDcuMzg5OCAxMi4yNjA3IDcuMTAzNTcgMTIuMzEyIDYuOTc5ODJDMTIuMzYzMiA2Ljg1NjA4IDEyLjQ4NyA2LjcwOTg0IDEyLjgxMzIgNi43OTk4M0MxMy4xNTgyIDYuODk0ODMgMTMuNDEwNyA3LjI3MjMxIDEzLjM4MTkgNy4zODQ4WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMTIuNTE0MyA5LjMwOTU5QzEyLjM3NTYgOS4yNzA4NCAxMi4yOTQzIDkuNDYwODMgMTIuMjA0MyA5LjU0NTgzQzEyLjA5MTggOS42NTMzMiAxMS42MTk0IDEwLjE5OTUgMTIuMjIxOCAxMC4zMzk1QzEyLjgyNTYgMTAuNDgwOCAxMi42NjA2IDkuODE1ODEgMTIuNjM4MSA5LjY4MDgyQzEyLjYyMzEgOS41ODcwOCAxMi42MTU2IDkuMzM4MzQgMTIuNTE0MyA5LjMwOTU5WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMTMuMTYxOSA4Ljk1NTc2QzEzLjExMTkgOS4wOTk1IDEzLjM5MzEgOS4zODMyNCAxMy41MjgxIDkuNTQ2OThDMTMuNjYzMSA5LjcxMDcyIDEzLjk2MTggOS44NjE5NiAxNC4xMzU2IDkuNTM1NzNDMTQuMzEwNiA5LjIwOTUgMTQuMDM5MyA5LjAwNzAxIDEzLjc5ODEgOC45NTA3NkMxMy41NTY5IDguODkzMjYgMTMuMjA2OSA4LjgyNTc3IDEzLjE2MTkgOC45NTU3NloiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTE1LjMxOTIgNy43OTU4MUMxNS4zMTkyIDcuODkyMDYgMTQuOTgxNyA4LjE2MjA0IDE0LjYwNDIgOC4xNzMyOUMxNC4zMjMgOC4xODIwNCAxNC4xMTQzIDguMDc5NTUgMTQuMTE0MyA3LjgwMjA2QzE0LjExNDMgNy41MjA4MyAxNC4zNjggNy4zNjMzMyAxNC42ODMgNy40MzA4M0MxNC44NjQyIDcuNDY4MzMgMTUuMzE5MiA3LjY0MzMyIDE1LjMxOTIgNy43OTU4MVoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTE0LjY2NTQgNi45Nzk2MkMxNC42OTQyIDcuMDkyMTEgMTUuMDMxNyA3LjEyNTg2IDE1LjI3MjkgNy4xMzIxMUMxNS41MTU0IDcuMTM4MzYgMTUuODEyOSA3LjA1MzM2IDE1LjcwMDQgNi43MjA4OEMxNS41ODc5IDYuMzg4NCAxNS4yNTU0IDYuNDczMzkgMTUuMTMxNiA2LjUzNDY0QzE1LjAwOTIgNi41OTU4OSAxNC42Mjc5IDYuODI3MTIgMTQuNjY1NCA2Ljk3OTYyWiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMTMuNjEzMSA0Ljc1NDdDMTMuNTExOSA0LjkwMDk0IDEzLjcwMzEgNS4yMDQ2OCAxMy44NzgxIDUuMzM5NjdDMTQuMDUzMSA1LjQ3NDY2IDE0LjI5NDMgNS41NTk2NiAxNC40MzU2IDUuMzIyMTdDMTQuNTc2OCA1LjA4NTkzIDE0LjMyODEgNC44NzIxOSAxNC4xNzY4IDQuNzc1OTVDMTQuMDI0MyA0LjY4MjIgMTMuNzExOSA0LjYxMjIxIDEzLjYxMzEgNC43NTQ3WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMTMuMDQ0NCA1LjAzMDk2QzEyLjk3ODEgNC45ODk3MSAxMi42MjgxIDUuMDc4NDUgMTIuNDI1NyA1LjI2MjE5QzEyLjIxNTcgNS40NTIxOCAxMi4xMzgyIDUuNzI5NjcgMTIuMzkxOSA1LjkyNzE2QzEyLjYzNjkgNi4xMTcxNSAxMi45MDMxIDUuOTMyMTYgMTMuMDEwNiA1LjY3OTY3QzEzLjExMTkgNS40NDIxOCAxMy4xMTk0IDUuMDc3MiAxMy4wNDQ0IDUuMDMwOTZaIiBmaWxsPSIjRjVFNUM3Ii8+CjxwYXRoIGQ9Ik0xMC4zMDgzIDQuODczNjNDMTAuMzc1OCA0Ljc3OTg5IDEwLjI1MjEgNC4yNzc0MiAxMC4wOTQ2IDQuMDg0OTNDOS45MzcxMSAzLjg5MzY5IDkuNzM5NjIgMy44MTk5NCA5LjUyMDg4IDMuOTgzNjhDOS4zNDgzOSA0LjExMjQyIDkuMjg0NjUgNC40NjI0MSA5LjUzMjEzIDQuNjQyNEM5LjgwMDg3IDQuODM5ODkgMTAuMjM0NiA0Ljk3NDg4IDEwLjMwODMgNC44NzM2M1oiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTEwLjUwNDQgNC4wOTYwN0MxMC41ODgyIDQuMTU3MzIgMTAuODkzMiA0LjExMzU3IDExLjEwMDYgMy45NzczM0MxMS4zMDgxIDMuODQxMDkgMTEuMzM5NCAzLjYyNDg1IDExLjIxOTQgMy40NzYxMUMxMS4wNzMxIDMuMjk2MTEgMTAuODM2OSAzLjM2MzYxIDEwLjY2ODIgMy41ODM2QzEwLjUyMDcgMy43NzQ4NCAxMC4zOTgyIDQuMDE3MzMgMTAuNTA0NCA0LjA5NjA3WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMTEuMDA1OSA0LjgyMjQzQzExLjA3MzQgNS4wMDI0MiAxMS43NDk2IDQuOTYzNjggMTEuOTQ1OCA0Ljg3ODY4QzEyLjE0MzMgNC43OTM2OSAxMi4yODA4IDQuNjE0OTUgMTIuMTcwOCA0LjM1NDk2QzEyLjA0NzEgNC4wNjI0NyAxMS44NTIxIDQuMDYxMjIgMTEuNzA5NiA0LjEwMTIyQzExLjQyODMgNC4xODEyMiAxMC45Mzk2IDQuNjQ2MTkgMTEuMDA1OSA0LjgyMjQzWiIgZmlsbD0iI0Y1RTVDNyIvPgo8ZGVmcz4KPHJhZGlhbEdyYWRpZW50IGlkPSJwYWludDBfcmFkaWFsXzcyMV8xNDUiIGN4PSIwIiBjeT0iMCIgcj0iMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJ0cmFuc2xhdGUoOS4wMjY1NSA2LjUxMTY2KSByb3RhdGUoLTAuMzc0NDY5KSBzY2FsZSg4LjE5NjczIDUuMzQ0MjcpIj4KPHN0b3Agb2Zmc2V0PSIwLjE2MTYiIHN0b3AtY29sb3I9IiNGMUIxNEEiLz4KPHN0b3Agb2Zmc2V0PSIwLjE2ODgiIHN0b3AtY29sb3I9IiNGMUIwNDkiLz4KPHN0b3Agb2Zmc2V0PSIwLjM3MDQiIHN0b3AtY29sb3I9IiNFODk4MjUiLz4KPHN0b3Agb2Zmc2V0PSIwLjUzNTQiIHN0b3AtY29sb3I9IiNFMjg4MTAiLz4KPHN0b3Agb2Zmc2V0PSIwLjY0MjUiIHN0b3AtY29sb3I9IiNFMDgzMDgiLz4KPHN0b3Agb2Zmc2V0PSIwLjcxMjUiIHN0b3AtY29sb3I9IiNFMTg2MEQiLz4KPHN0b3Agb2Zmc2V0PSIwLjc5MjQiIHN0b3AtY29sb3I9IiNFNTkwMUIiLz4KPHN0b3Agb2Zmc2V0PSIwLjg3NyIgc3RvcC1jb2xvcj0iI0VCQTAzMSIvPgo8c3RvcCBvZmZzZXQ9IjAuOTQ3IiBzdG9wLWNvbG9yPSIjRjFCMTRBIi8+CjwvcmFkaWFsR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNzg4NTcgMTQuNTI3QzMuNzg4NTcgMTQuNTI3IDYuMTkwOTUgMTcuMDU0MyAxMC4zMTIgMTYuNDEzMUMxNC40MzMgMTUuNzczMSAxNi4wODU0IDEyLjE1OTYgMTYuNDI1NCAxMC4wOTk3QzE2Ljg1NzkgNy40NzYwOCAxNS42NTkyIDUuNjcyNDIgMTUuNTk1NSA1LjU3MzY4QzE1LjU0OCA1LjUwMTE4IDMuNzg4NTcgMTQuNTI3IDMuNzg4NTcgMTQuNTI3WiIgZmlsbD0iI0RDNTgxOSIvPgo8cGF0aCBkPSJNMy42MDEwNCAzLjY4ODU3QzEuMTMyNDIgNi4yODcxOSAwLjcxMzY5MyAxMC4wODMyIDIuNTY5ODQgMTMuMDE2OEMzLjQzMzU1IDE0LjM4MTggMy45NjIyNyAxNC42NTkyIDMuOTYyMjcgMTQuNjU5MkMzLjk2MjI3IDE0LjY1OTIgOC4wMDA4MSAxNy4yNDkxIDEyLjUzOTMgMTMuMzIzMUMxNi40MjY2IDkuOTYwNzQgMTUuNjAyOSA1LjU4MjIyIDE1LjYwMjkgNS41ODIyMkMxNS42MDI5IDUuNTgyMjIgMTUuMTE3OSA0LjUyMjI4IDE0LjA5OCAzLjUxNjA4QzExLjgzNDQgMS4yODEyIDYuNzc0NjIgMC4zNDYyNTEgMy42MDEwNCAzLjY4ODU3WiIgZmlsbD0iI0VGNkMzMiIvPgo8cGF0aCBkPSJNMTMuNDk1NiAzLjAwNDk4QzEzLjQ1OTQgMy4wMDc0OCA5Ljg0ODMyIDMuMjY3NDYgNi4wMjg1MiA4LjQwMjE5QzMuNjE2MTUgMTEuNjQ0NSAzLjczNDg5IDEzLjkzMzIgMy44Mjg2NCAxNC41NjA2QzMuODI4NjQgMTQuNTYwNiAzLjY0NDkgMTQuNDA5NCAzLjUxNDkgMTQuMjY5NEMzLjM4NDkxIDE0LjEzMDYgMy4yODM2NiAxNC4wMDY5IDMuMjgzNjYgMTQuMDA2OUMzLjI3MzY2IDEyLjkwODIgMy42MDI0IDEwLjgyNTggNS42Mjg1NCA4LjEwMzQ2QzguNzEyMTMgMy45NTQ5MyAxMS42NjA3IDIuODczNzQgMTIuODk2OSAyLjU5Mzc1QzEyLjg5NjkgMi41OTM3NSAxMy4wNDgxIDIuNjgyNSAxMy4xOTQ0IDIuNzczNzRDMTMuMzAxOSAyLjg0MTI0IDEzLjQ5NTYgMy4wMDQ5OCAxMy40OTU2IDMuMDA0OThaIiBmaWxsPSIjMkQzMTMwIi8+CjxwYXRoIGQ9Ik0yLjk0NjE2IDQuNDcyNDFDMy4zNDczOCA1LjEzMTEzIDQuMzE2MDggNi41NTQ4IDUuOTkzNDkgOC4wNDA5N0M4LjEwNDYzIDkuOTEzMzcgMTAuMjk5NSAxMS40MzU4IDEyLjUxNDQgMTIuNTY0NUMxMy41NjA2IDEzLjA5ODIgMTQuMzY0MyAxMy40MTA3IDE0LjkzMyAxMy41OTQ0QzE0LjkzMyAxMy41OTQ0IDE0Ljg1MDUgMTMuNzIwNyAxNC43NzMgMTMuODI0NEMxNC42OTU1IDEzLjkyODIgMTQuNjEwNSAxNC4wMTU3IDE0LjYxMDUgMTQuMDE1N0MxNC4wMzE4IDEzLjgxOTQgMTMuMjU5NCAxMy41MDY5IDEyLjI4NTcgMTMuMDA5NUMxMC4wMzMzIDExLjg2MDggNy44MDM0IDEwLjMxNDYgNS42NjEwMSA4LjQxNDdDNC4wNzM2IDcuMDA3MjggMy4xMTExNSA1LjY3NDg1IDIuNjM4NjcgNC45Mjg2NEMyLjYzODY3IDQuOTI4NjQgMi43MDQ5MiA0Ljc5NzM5IDIuNzkyNDEgNC42ODExNUMyLjg3OTkxIDQuNTY0OTEgMi45NDYxNiA0LjQ3MjQxIDIuOTQ2MTYgNC40NzI0MVoiIGZpbGw9IiMyRDMxMzAiLz4KPHBhdGggZD0iTTcuNjQ3MTkgMS41OTYxOUM3LjQ2ODQ1IDEuNzk5OTMgNy4yMTM0NiAyLjEzMTE2IDYuOTE1OTggMi42MzIzOUM2LjY0MzQ5IDMuMDg5ODYgNi41MDM1IDMuNTQ0ODQgNi4zNDEwMSA0LjA3MjMxQzYuMDc0NzcgNC45MzcyNyA1Ljc3MzUzIDUuOTE3MjEgNC43ODczNCA3LjE2OTY1QzMuNDE3NDEgOC45MDk1NiAyLjA3MTIzIDkuNDkwNzggMS40NDM3NiA5LjY3NzAyQzEuNDQzNzYgOS42NzcwMiAxLjQyNjI2IDkuNTE0NTIgMS40MjAwMiA5LjQwMDc4QzEuNDEzNzcgOS4yODcwNCAxLjQxNjI3IDkuMTU4MjkgMS40MTYyNyA5LjE1ODI5QzIuMDEyNDggOC45NTQ1NSAzLjE5MjQyIDguMzg0NTggNC4zOTM2MSA2Ljg2MDkxQzUuMzIyMzEgNS42ODA5OCA1LjYwODU0IDQuNzQ4NTMgNS44NjIyOCAzLjkyNjA3QzYuMDI3MjcgMy4zOTExIDYuMTgyMjYgMi44ODYxMiA2LjQ4NiAyLjM3NzRDNi42Mjk3NCAyLjEzNjE2IDYuNzY0NzMgMS45Mjk5MiA2Ljg4ODQ4IDEuNzU3NDNDNi44ODg0OCAxLjc1NzQzIDcuMDI3MjIgMS43MTExOSA3LjIxNzIxIDEuNjY4NjlDNy4zNjQ3IDEuNjMzNjkgNy42NDcxOSAxLjU5NjE5IDcuNjQ3MTkgMS41OTYxOVoiIGZpbGw9IiMyRDMxMzAiLz4KPHBhdGggZD0iTTkuNTg5NTggMTYuNTAwNEM5LjE3MDg1IDE2LjEyMTcgOC4zMDU4OSAxNS4yNTY4IDcuNjc0NjggMTQuMDUzMUM2Ljk3NTk2IDEyLjcyMTkgNi42NDg0OCAxMC43MTk1IDcuNTgwOTMgOS40NjgzMUM4LjY0NTg4IDguMDQwODggMTAuNjM0NSA3Ljg0NzE0IDEyLjQ0MTkgNy44NDcxNEMxMi40NDQ0IDcuODQ3MTQgMTIuNDQ2OSA3Ljg0NzE0IDEyLjQ0OTQgNy44NDcxNEMxNC43NTA2IDcuODQ4MzkgMTYuNDgxNyA4LjMyOTYyIDE2LjUwMTcgOC4zMzU4N0MxNi41MDE3IDguMzM1ODcgMTYuNDg4IDguMTY3MTIgMTYuNDY5MiA4LjAyMzM4QzE2LjQ1MyA3Ljg5OTY0IDE2LjQzNTUgNy43ODQ2NCAxNi40MzU1IDcuNzg0NjRDMTYuMzQ4IDcuNzYyMTUgMTQuODE4MSA3LjM0ODQyIDEyLjQ0OTQgNy4zNDcxN0MxMi40NDY5IDcuMzQ3MTcgMTIuNDQ0NCA3LjM0NzE3IDEyLjQ0MTkgNy4zNDcxN0MxMC41MTA4IDcuMzQ3MTcgOC4zNzU4OSA3LjU2NDY2IDcuMTc5NyA5LjE2OTU3QzYuNDA5NzQgMTAuMjAyIDYuMTI3MjYgMTIuMTg0NCA3LjIzMjIgMTQuMjg1NkM3Ljc1MjE3IDE1LjI3NTUgOC40MDcxNCAxNi4wMzkyIDguODg1ODYgMTYuNTIwNEM4Ljg4NTg2IDE2LjUyMDQgOS4wMjU4NiAxNi41MjU0IDkuMjA0NiAxNi41MjI5QzkuMzgwODQgMTYuNTIwNCA5LjU4OTU4IDE2LjUwMDQgOS41ODk1OCAxNi41MDA0WiIgZmlsbD0iIzJEMzEzMCIvPgo8L3N2Zz4K', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuOTc1OTIgMS40NDAwNUM2Ljk2NzE3IDEuNjIwMDUgNi44MDcxOCAxNi41MTU1IDYuODI1OTMgMTYuNTkxOEM2Ljg0NDY4IDE2LjY2OCA3LjkzOTYyIDE2LjY0NjggNy45Mzk2MiAxNi42MDNDNy45Mzk2MiAxNi41MDY4IDguMTMzMzYgMS41NDg4IDguMTI5NjEgMS40NTEzQzguMTI1ODYgMS4zOTEzMSA2Ljk3ODQyIDEuMzcyNTYgNi45NzU5MiAxLjQ0MDA1WiIgZmlsbD0iI0ZGRDUxRCIvPgo8cGF0aCBkPSJNNi45NzM0MSAxLjU1MzcxQzYuOTczNDEgMS41NTM3MSA3LjM1MzM5IDIuMDY4NjggNy41OTA4OCAyLjM3NzQyQzcuODAzMzcgMi42NTM2NSA4LjExMjEgMy4wMDM2MyA4LjExMjEgMy4wMDM2M0w4LjEwMjEgMy43NTM1OUM4LjEwMjEgMy43NTM1OSA3LjYyMjEzIDMuMjE2MTIgNy4zNzk2NCAyLjkyNzM5QzcuMTE3MTUgMi42MTYxNSA2Ljk2NDY2IDIuMzY4NjcgNi45NjQ2NiAyLjM2ODY3TDYuOTczNDEgMS41NTM3MVoiIGZpbGw9IiNGREE3MjYiLz4KPHBhdGggZD0iTTYuOTM5NjkgNC4zMzg2MkM2LjkzOTY5IDQuMzM4NjIgNy4xODIxOCA0Ljc2NDg1IDcuNDk3MTYgNS4xNjg1OEM3Ljc1MzQgNS40OTczMSA4LjA3MzM4IDUuNzkzNTUgOC4wNzMzOCA1Ljc5MzU1TDguMDU3MTMgNy4yNzU5N0w2LjkwMzQ0IDcuMDU5NzNMNi45Mzk2OSA0LjMzODYyWiIgZmlsbD0iI0ZEQTcyNiIvPgo8cGF0aCBkPSJNNi44NjQ3MSAxMS42MDk0TDYuODYwOTYgMTIuMTMwNkM2Ljg2MDk2IDEyLjEzMDYgNy4wOTA5NSAxMi41MTQ0IDcuMzgzNDMgMTIuODQ2OEM3LjYyNzE3IDEzLjEyMzEgNy45Nzg0IDEzLjQ5NjggNy45Nzg0IDEzLjQ5NjhMOC4wMDIxNSAxMS42MDk0TDcuNTI5NjggMTEuMzc4Mkw2Ljg2NDcxIDExLjYwOTRaIiBmaWxsPSIjRkRBNzI2Ii8+CjxwYXRoIGQ9Ik02Ljg0MDkzIDE0LjE3MTlDNi44NDA5MyAxNC4xNzE5IDcuMjA5NjcgMTQuNjUzMSA3LjQ2NTkgMTQuOTQwNkM3LjY2OTY0IDE1LjE2OTMgNy45NTQ2MyAxNS40NjkzIDcuOTU0NjMgMTUuNDY5M0w3Ljk0NDYzIDE2LjE4NTVDNy45NDQ2MyAxNi4xODU1IDcuNDY1OSAxNS43MDE4IDcuMjQ5NjYgMTUuNDY5M0M3LjAzMzQyIDE1LjIzNjggNi44MzM0NCAxNS4wMDQzIDYuODMzNDQgMTUuMDA0M0w2Ljg0MDkzIDE0LjE3MTlaIiBmaWxsPSIjRkRBNzI2Ii8+CjxwYXRoIGQ9Ik0yLjEwNjIxIDEzLjQyMDdDMi4xMDYyMSAxMy40MjA3IDEuOTE4NzIgMTMuMTQwNyAyLjAzMzcxIDEyLjc4MkMyLjE1NDk2IDEyLjQwMiAyLjQyOTk0IDEyLjI2MiAyLjU1ODY5IDExLjk1ODNDMi43MzYxOCAxMS41MzgzIDIuNjk0OTMgMTEuMTk4MyAzLjAyNzQxIDEwLjg4MzNDMy4zMjYxNSAxMC42MDA4IDMuNzM4NjIgMTAuNzA1OCA0LjEwMjM2IDEwLjYwMDhDNC40NjYwOSAxMC40OTU4IDQuNjE5ODMgMTAuMjc3MSA0LjYxOTgzIDEwLjI3NzFMNS40Njg1MyAxMC41MzU4QzUuNDY4NTMgMTAuNTM1OCA1LjM1NjA0IDExLjQ4MDggNS4wMTYwNiAxMS40NjQ1QzQuNjc2MDggMTEuNDQ4MyAzLjUwNDg5IDExLjY2NyAzLjQ4MTE0IDExLjcxNDVDMy40NTczOSAxMS43NjMzIDIuNDE0OTQgMTMuNDAzMiAyLjQxNDk0IDEzLjQwMzJMMi4xMDYyMSAxMy40MjA3WiIgZmlsbD0iIzhBODY3RSIvPgo8cGF0aCBkPSJNOS45OTk1MSAxMS4zNjgyQzEwLjAxNTggMTEuNDQwNyAxMC4yOTA3IDExLjgzNjkgMTAuNzI3IDEyLjE5MTlDMTEuMTYzMiAxMi41NDY5IDExLjgwOTQgMTIuODA1NiAxMi4wMjgyIDEyLjk2ODFDMTIuMjQ2OSAxMy4xMjk0IDEyLjQwODEgMTMuMjAxOSAxMi42NzQ0IDEzLjU1ODFDMTIuOTQwNiAxMy45MTQzIDEzLjAyOTQgMTQuMTMxOCAxMy4yMjQzIDE0LjQwNjhDMTMuNDE4MSAxNC42ODE4IDEzLjYyMDYgMTQuODc1NSAxMy43OTgxIDE1LjAxM0MxMy45ODE4IDE1LjE1NDMgMTQuMzU1NSAxNS4zNzY3IDE0LjQyMDUgMTUuMzY4QzE0LjQ4NTUgMTUuMzYwNSAxNC43NjggMTQuNzcwNSAxNC43NjggMTQuNzcwNUMxNC43NjggMTQuNzcwNSAxMy45MTQzIDEzLjc5ODEgMTMuNzIzMSAxMy40NjgxQzEzLjUzMTggMTMuMTM5NCAxMy4xNDkzIDEyLjYxODEgMTIuNzI0NCAxMi4zNzQ0QzEyLjUxMTkgMTIuMjUxOSAxMS4xMjMyIDEwLjUyMDggMTEuMTIzMiAxMC41MjA4TDkuOTk5NTEgMTEuMzY4MloiIGZpbGw9IiM4QTg2N0UiLz4KPHBhdGggZD0iTTEyLjY3MzIgNy42ODcyN0MxMi42NzMyIDcuNjg3MjcgMTIuODExOSA3LjUyNDc4IDEyLjkwMTkgNy40NjIyOEMxMy4wMTU2IDcuMzgyMjggMTMuMTA4MSA3LjMzNjA0IDEzLjI4ODEgNy4zMzIyOUMxMy40NTA2IDcuMzI5NzkgMTMuOTUwNiA3Ljc2NDc2IDEzLjk1MDYgNy43NjQ3NkMxMy45NTA2IDcuNzY0NzYgMTUuMTMzIDkuOTU1OSAxNS4xMzY4IDkuOTUyMTVDMTUuMTQwNSA5Ljk0ODQgMTUuOTA1NSAxMC44MjQ2IDE1Ljg5MTcgMTAuODA3MUMxNS44NzggMTAuNzg4NCAxNi4zNjE3IDExLjAzNzEgMTYuMzYxNyAxMS4wMzcxQzE2LjM2MTcgMTEuMDM3MSAxNi4yNzkyIDExLjU4MjEgMTUuMzE0MyAxMS41NTMzQzE0LjM3MDYgMTEuNTI1OCAxMy43NzQzIDEwLjk4OTYgMTMuNDM5NCAxMC4yNzcxQzEzLjI3ODEgOS45MzQ2NSAxMy4yNTMxIDkuMzEzNDMgMTMuMjUzMSA5LjMxMzQzTDEyLjY3MzIgNy42ODcyN1oiIGZpbGw9IiM3QTU3QkQiLz4KPHBhdGggZD0iTTEzLjA1ODEgNy4zNzA5QzEzLjA1ODEgNy4zNzA5IDEzLjEyNTYgNy42NzIxNCAxMy4zNzA2IDcuODg5NjNDMTMuNjE1NiA4LjEwNTg3IDEzLjgzMDYgOC4yOTU4NiAxNC4wMDA2IDguNTY4MzRDMTQuMTcwNSA4Ljg0MDgzIDE0LjU1NDMgOS44MzQ1MiAxNS4wNDY4IDEwLjQxNDVDMTUuNTM5MiAxMC45OTQ1IDE1LjcyNTUgMTEuMDMyIDE1LjkxNjcgMTEuMTAyQzE2LjA3NzkgMTEuMTYwNyAxNi4yMzQyIDExLjE1NDUgMTYuMjkxNyAxMS4xMzJDMTYuMzgwNCAxMS4wOTk1IDE2LjQ2NDIgMTAuOTI3IDE2LjA1OTIgMTAuNTUzMkMxNS42NTQyIDEwLjE3OTUgMTUuNDAwNSA5LjczNzAzIDE1LjE3OCA5LjI5MjA1QzE0Ljk1NTUgOC44NDcwOCAxNC44NDQzIDcuODUzMzggMTQuMjMzIDcuNDQwOUMxMy42MjE4IDcuMDI5NjcgMTMuMDU4MSA3LjM3MDkgMTMuMDU4MSA3LjM3MDlaIiBmaWxsPSIjQTQ3RkNDIi8+CjxwYXRoIGQ9Ik0xMy4wNDE4IDcuOTI4NDdDMTMuMDQxOCA3LjkyODQ3IDEzLjQyOTMgOC40MzIxOSAxMy40MTMxIDkuMTIyMTVDMTMuMzk2OCA5LjgxMjEyIDEzLjA2MDYgMTAuMzAwOCAxMy4wNjA2IDEwLjcwOTZDMTMuMDYwNiAxMS4xMTgzIDEzLjE0OTMgMTEuMzk5NSAxMy41NDkzIDExLjUxOTVDMTMuOTUwNSAxMS42Mzk1IDE0LjE2NjggMTEuNzEyIDE0LjIzOTMgMTEuODgwOEMxNC4zMTE4IDEyLjA0OTUgMTQuMjk1NSAxMi40NzQ1IDE0LjQ3OTMgMTIuODI3QzE0LjY2MyAxMy4xNzk0IDE0LjgwMyAxMy4yNjA3IDE0Ljg4OCAxMy40NDQ0QzE0Ljk4NDIgMTMuNjQ5NCAxNS4wMDA1IDEzLjk4OTQgMTUuMDAwNSAxMy45ODk0QzE1LjAwMDUgMTMuOTg5NCAxNC44MDA1IDE0Ljk1OTMgMTQuNTY4IDE0LjU1MDZDMTQuMzM1NSAxNC4xNDE5IDEzLjUzMzEgMTIuNDgyIDEzLjMwOTMgMTIuMjg5NUMxMy4wODQzIDEyLjA5NyAxMS44ODE5IDEwLjc3MzMgMTEuODgxOSAxMC43MDIxQzExLjg4MTkgMTAuNjMwOCAxMy4wNDE4IDcuOTI4NDcgMTMuMDQxOCA3LjkyODQ3WiIgZmlsbD0iI0FDQTM5OSIvPgo8cGF0aCBkPSJNMi43NzYxNSAxMy41ODY5TDMuMjMxMTMgMTMuNjUwN0wzLjI1MjM4IDEzLjMyMkMzLjI1MjM4IDEzLjMyMiAzLjQwMTEyIDEzLjAxNDUgMy41NDg2MSAxMi44MTMyQzMuNjk3MzUgMTIuNjEyIDMuODM0ODUgMTIuMzg5NSA0LjExOTgzIDEyLjM0N0M0LjQwNjA3IDEyLjMwNDUgNi4yMTU5NyAxMi4zNDcgNi4zNjQ3MSAxMi4yNjJDNi41MTM0NiAxMi4xNzcgNi44MTk2OSAxMS43OTU4IDYuODE5NjkgMTEuNzk1OEM2LjgxOTY5IDExLjc5NTggNy45MTMzOCAxMS44OTA4IDkuMDQzMzIgMTEuNjU4M0M5Ljg0ODI4IDExLjQ5MiAxMC4zNzcgMTEuNDM1OCAxMC4zNzcgMTEuNDM1OEwxMS4xNzA3IDExLjM2MjFDMTEuMTcwNyAxMS4zNjIxIDEyLjAwNjkgMTAuOTkyMSAxMS45ODU3IDEwLjkxNzFDMTEuOTY0NCAxMC44NDMzIDExLjMzOTUgOS43MDk2NCAxMS4yNzU3IDkuNzQyMTRDMTEuMjEyIDkuNzczMzkgNi40MzcyMSAxMC4yMTg0IDYuMzYzNDYgMTAuMzAzNEM2LjI4OTcyIDEwLjM4ODQgNS40MjEwMSAxMS4yNzcxIDUuNDIxMDEgMTEuMjc3MUM1LjQyMTAxIDExLjI3NzEgMy41MTQ4NiAxMS43NzQ1IDMuNDgzNjIgMTEuNzg1OEMzLjQ1MjM3IDExLjc5NTggMi42NTc0MSAxMy40NDgyIDIuNjU3NDEgMTMuNDQ4MkwyLjc3NjE1IDEzLjU4NjlaIiBmaWxsPSIjQUNBMzk5Ii8+CjxwYXRoIGQ9Ik00LjAyNjA3IDkuMjM3MDZDNC4wMjYwNyA5LjIzNzA2IDQuMDIxMDcgOC44NDgzMyA0LjA4MjMyIDguMzYzMzZDNC4xNDEwNiA3Ljg5MzM4IDQuMjQ5ODEgNy4zNzIxNiA0LjI2MTA2IDYuOTY5NjhDNC4yNzM1NiA2LjUyMDk1IDQuMjIyMzEgNi4yNTQ3MiA0LjE3NDgxIDYuMjM5NzJDNC4xMjg1NyA2LjIyNDcyIDMuOTAyMzMgNi43NTk2OSAzLjkwMjMzIDYuNzU5NjlDMy45MDIzMyA2Ljc1OTY5IDMuOTE2MDggNy4wMDIxOCAzLjgxMjMzIDcuMjQwOTJDMy42ODk4NCA3LjUyMjE1IDMuNDk0ODUgNy40MjIxNiAzLjQ5NDg1IDcuNDIyMTZMMy4yNjM2MSA2LjYzNzJMMy43MTQ4NCA1LjM5MTAxTDMuOTU2MDcgMy42OTM2TDQuNzU4NTMgNC4yNjIzMkw1LjI5NiA2LjQwNDcxTDUuMTA4NTEgOC40MzcxTDQuNTY5NzkgOS40NDk1NUw0LjAyNjA3IDkuMjM3MDZaIiBmaWxsPSIjQUNBMzk5Ii8+CjxwYXRoIGQ9Ik0xMS4yMjQ1IDcuMDUzNDdMMTAuNjk4MiA4LjMxODRMMTEuMjg3IDExLjAxOTVMMTAuNzgzMiAxMS40MTJDMTAuNzgzMiAxMS40MTIgMTEuMTUzMiAxMi4xMjE5IDExLjcyOTQgMTIuNDAxOUMxMi4zMDY5IDEyLjY4MTkgMTIuODIwNiAxMi42MzY5IDEyLjk5MTkgMTIuNjI2OUMxMy4xNjMxIDEyLjYxODIgMTMuMjc4MSAxMi42OTE5IDEzLjM3OTQgMTIuODM0NEMxMy44NTkzIDEzLjUwMzEgMTMuODMwNiAxNC4xMDkzIDEzLjk2NDMgMTQuMzU4MUMxNC4xOTgxIDE0Ljc5MDYgMTQuNDg4IDE0LjgyNjggMTQuNDg4IDE0LjgyNjhDMTQuNDg4IDE0LjgyNjggMTUuMDEwNSAxNC42MzY4IDE0Ljk5MyAxNC42MDE4QzE0Ljk3NTUgMTQuNTY1NiAxNC43NDkzIDE0LjE1OTMgMTQuNzQ5MyAxNC4xNTkzQzE0Ljc0OTMgMTQuMTU5MyAxNC42NTkzIDEzLjc2MzEgMTQuNDUxOCAxMy40MDE5QzE0LjI0NDMgMTMuMDQwNyAxNC4xMjY4IDEyLjg2MDcgMTQuMDczMSAxMi40ODE5QzE0LjAxOTMgMTIuMTAzMiAxNC4wMDA2IDExLjkyMzIgMTMuNTk1NiAxMS44MzMyQzEzLjE4OTQgMTEuNzQzMiAxMi42NjY5IDExLjQ5MDcgMTIuNzAzMSAxMC45NzdDMTIuNzM5NCAxMC40NjMzIDEzLjM4NDQgOS4zNzA4NCAxMy4yODk0IDguNTc4MzlDMTMuMjA4MSA3LjkwMjE3IDEyLjYyMTkgNy4yNjIyMSAxMS43MDE5IDcuMTE3MjFDMTEuNDQ4MiA3LjA3NzIyIDExLjIyNDUgNy4wNTM0NyAxMS4yMjQ1IDcuMDUzNDdaIiBmaWxsPSIjRjFFREVDIi8+CjxwYXRoIGQ9Ik0xNS4wMTE4IDEzLjc5MDdDMTQuOTU1NSAxMy44MDA3IDE0LjgxMyAxNC4xNDE5IDE0LjcwNTUgMTQuMzIzMUMxNC42MjA2IDE0LjQ2NTYgMTQuMzk5MyAxNC43OTE5IDE0LjQxNjggMTQuODM2OUMxNC40MzQzIDE0Ljg4MTkgMTUuMTM5MyAxNS41NjMxIDE1LjE4MyAxNS41OTQzQzE1LjI0NjggMTUuNjM5MyAxNS4zNzMgMTUuNTk0MyAxNS40MjY4IDE1LjM5NTZDMTUuNDgwNSAxNS4xOTY4IDE1Ljc0MyAxNC4wNjA3IDE1Ljc0MyAxNC4wNjA3QzE1Ljc0MyAxNC4wNjA3IDE1LjEyMDUgMTMuNzcxOSAxNS4wMTE4IDEzLjc5MDdaIiBmaWxsPSIjNUU2MzY3Ii8+CjxwYXRoIGQ9Ik0xMS44MTA3IDcuNTY3MjJDMTEuNzY0NCA3LjcwNDcxIDExLjg5MTkgNy44MDIyMSAxMi4xMTY5IDguMDE4NDVDMTIuMzQxOSA4LjIzNDY4IDEyLjQ3ODIgOC40OTU5MiAxMi41MDQ0IDguODAzNEMxMi41MzE5IDkuMTA5NjQgMTIuNTgzMSA5LjMzNzEzIDEyLjc0ODEgOS4zNDQ2M0MxMi45NDY5IDkuMzUzMzcgMTMuMTY2OSA4Ljg5MzQgMTIuODI5NCA4LjIwODQzQzEyLjQ2OTQgNy40NzcyMiAxMS44MzgyIDcuNDg1OTcgMTEuODEwNyA3LjU2NzIyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTExLjI5NTcgNy4yMzczTDExLjI3ODIgMTAuOTYyMUMxMS4yNzgyIDEwLjk2MjEgMTAuOTY3IDExLjA4MzQgMTAuNTc3IDEwLjk2MjFDMTAuMTg3IDEwLjg0MDkgMTAuMDU3IDEwLjU5ODQgMTAuMDU3IDEwLjU5ODRDMTAuMDU3IDEwLjU5ODQgOS43Nzk1NSAxMS4wNjU5IDkuMDI1ODQgMTEuMDU3MUM4LjI3MjEzIDExLjA0ODQgOC4wODIxNCAxMC42OTM0IDguMDgyMTQgMTAuNjkzNEM4LjA4MjE0IDEwLjY5MzQgNy45NTA4OSAxMC45ODU5IDcuMzk4NDIgMTEuMDU3MUM3LjA4NTk0IDExLjA5NzEgNi44MTk3IDExLjA5NzEgNi43NjcyIDEwLjk3NTlDNi43MTQ3MSAxMC44NTQ2IDUuODQxIDguMTQ0NzYgNS44NDEgOC4xNDQ3Nkw4LjgwMDg1IDcuMzc2MDVMMTEuMjk1NyA3LjIzNzNaIiBmaWxsPSIjQjBFNEZFIi8+CjxwYXRoIGQ9Ik03LjE2NDY4IDguMjMzNDVMNy4xMzg0MyAxMC42MDU4QzcuMTM4NDMgMTAuNjA1OCA3LjM5NzE2IDEwLjYwODMgNy42MzIxNSAxMC40NDk2QzcuODc3MTQgMTAuMjgzMyA3Ljk4NzEzIDEwLjExNzEgOC4wMzA4OCAxMC4xMjA5QzguMDkwODggMTAuMTI1OSA4LjQwMzM2IDEwLjYwNzEgOS4wMDA4MyAxMC42MjMzQzkuNjY4MjkgMTAuNjQwOCA5Ljk2MjAzIDEwLjA2OTYgMTAuMDE0NSAxMC4wNjk2QzEwLjA4MzMgMTAuMDY5NiAxMC4yMjk1IDEwLjMxNzEgMTAuMzc4MyAxMC4zOTgzQzEwLjY0NyAxMC41NDU4IDEwLjg0NTcgMTAuNDc1OCAxMC44NDU3IDEwLjQ3NThMMTAuODU0NSA3Ljc4MjIzTDcuMTY0NjggOC4yMzM0NVoiIGZpbGw9IiMzNEI2RTIiLz4KPHBhdGggZD0iTTIuMDE5OTQgNC40Njk5NEMyLjAxOTk0IDQuNDY5OTQgMS45MDEyIDMuNzI4NzMgMi4xNzc0NCAzLjI5Mzc1QzIuNDUzNjcgMi44NTg3NyAyLjg3ODY1IDIuNzQwMDMgMi44Nzg2NSAyLjc0MDAzTDMuNjU5ODYgMi41NDI1NEMzLjY1OTg2IDIuNTQyNTQgMy45NjIzNCAyLjQxNjI5IDQuNTM5ODEgMi40MzM3OUM1LjEwOTc4IDIuNDUxMjkgNS41OTg1MSAyLjc0Mzc4IDUuODYzNDkgMi44OTg3N0M2LjE2OTczIDMuMDc2MjYgNi40NTU5NiAzLjQ2MjQ5IDYuMzU3MjIgMy42Mjk5OEM2LjI1ODQ3IDMuNzk3NDcgNS45OTIyMyAzLjY3OTk4IDUuOTkyMjMgMy42Nzk5OEM1Ljk5MjIzIDMuNjc5OTggNi43MTM0NSA0LjE4MzcgNy4wNTk2OCA1LjA5MjRDNy4zNDcxNiA1Ljg0ODYxIDcuMzg1OTEgNi42MTM1NyA3LjM4NTkxIDYuNjEzNTdMOC4zMTQ2MSA4LjA2NkwxMS4xMjA3IDYuNjgyMzJDMTEuMTIwNyA2LjY4MjMyIDExLjMwOTUgNi42NzEwNyAxMS40MjY5IDYuNzYxMDdDMTEuNTk0NCA2Ljg4OTgxIDExLjYyNTcgNy4wOTQ4IDExLjYyNTcgNy4wOTQ4QzExLjYyNTcgNy4wOTQ4IDExLjQ0NDQgNy4xNDQ3OSAxMS4yNDgyIDcuNDIzNTNDMTAuNzA0NSA4LjE5NDc0IDEwLjM4ODMgMTAuMDUyMSA4Ljg4NzA4IDkuOTIzNEM3LjU4MjE1IDkuODEyMTUgNy4zNTU5MSA4LjUwMDk3IDcuMjY1OTIgOC4xNjQ3NEM3LjE3NzE3IDcuODI4NTEgNi4wMDg0OCA2Ljc1NzMyIDYuMDA4NDggNi43NTczMkw0Ljg5NDc5IDQuMjEyNDVMMy43MTg2IDMuMTM1MDFMMi40NDM2NyA0LjM4OTk0TDIuMDE5OTQgNC40Njk5NFoiIGZpbGw9IiM3QTU3QkQiLz4KPHBhdGggZD0iTTQuNjQ5ODIgMTAuOTIyMUM0LjY0OTgyIDEwLjkyMjEgNC4zOTczMyAxMS4wNjU4IDQuMDcyMzUgMTEuMTEyMUMzLjc0NzM2IDExLjE1NzEgMy40MDQ4OCAxMS4xMDMzIDMuMTQzNjUgMTEuMzgyMUMyLjg4MjQxIDExLjY2MDggMi45ODk5IDEyLjA1ODMgMi43ODI0MSAxMi4zNjQ1QzIuNTc0OTMgMTIuNjcwOCAyLjQwMzY4IDEyLjg2MDcgMi4yMzI0NCAxMy4wNDk1QzIuMDYxMiAxMy4yMzgyIDIuMDI0OTUgMTMuNDU1NyAyLjAzMzcgMTMuNjM1N0MyLjA0MjQ1IDEzLjgxNTcgMi4xOTYyIDE0LjEzMTkgMi4xOTYyIDE0LjEzMTlMMy4wNjI0IDE0LjA2ODJMMy4wNTM2NSAxMy41MDk1QzMuMDUzNjUgMTMuNTA5NSAyLjkzNzQxIDEzLjMwODIgMy4wOTg2NSAxMy4wMzJDMy4yMDIzOSAxMi44NTQ1IDMuNDg2MTMgMTIuMzgzMyAzLjU3NjEyIDEyLjI2NThDMy42NjYxMiAxMi4xNDgzIDMuNzI2MTIgMTIuMDEwOCAzLjkzNzM1IDExLjk3N0M0LjE2NjA5IDExLjkzOTUgNS45OTM1IDExLjkyMzMgNi4xNjQ3NCAxMS44NDIxQzYuMzM1OTggMTEuNzYwOCA2LjcwNTk2IDExLjM2NDYgNi43MzM0NiAxMS4wNjcxQzYuNzYwOTYgMTAuNzY5NiA2Ljc3ODQ1IDguMzUyMjQgNi43Nzg0NSA4LjM1MjI0TDYuOTQ3MiA3LjAyMTA2QzYuOTQ3MiA3LjAyMTA2IDYuODQ4NDUgNi43NTEwNyA2Ljc0NzIxIDYuNDc5ODNDNi40MDQ3MiA1LjU1OTg4IDUuOTg0NzUgNC41OTc0MyA1LjE4OTc5IDMuNjgyNDhDNC40MDg1OCAyLjc4Mzc4IDMuNjIyMzcgMi44Nzg3NyAzLjYyMjM3IDIuODc4NzdDMy42MjIzNyAyLjg3ODc3IDIuNDA0OTMgMS43NzAwOCAyLjM0MTE5IDEuNjc4ODRDMi4yNzc0NCAxLjU4NzU5IDIuMTg4NyAxLjQ5ODg1IDIuMTY5OTUgMS41NzAwOUMyLjE1MTIgMS42NDEzNCAyLjIwNjIgMi40MTc1NSAyLjIwNjIgMi40MTc1NUwyLjY1NzQyIDMuMTkyNTFDMi42NTc0MiAzLjE5MjUxIDIuNTY3NDMgMy41NjI0OSAyLjQxMzY4IDMuOTMyNDdDMi4yNTk5NCA0LjMwMjQ1IDIuMDE5OTYgNC40Njg2OSAyLjAxOTk2IDQuNDY4NjlMMS45Mjg3MSA2LjEwODZDMS45Mjg3MSA2LjEwODYgMS43Mzc0NyA2LjQwNzM0IDEuNzMxMjIgNi43OTYwN0MxLjcyNDk3IDcuMTc4NTUgMy4yNDk4OSA2Ljc1NjA3IDMuMjU2MTQgNi43MjM1N0MzLjI2MjM5IDYuNjkxMDcgMy43NzczNiA1LjI4ODY1IDMuNzcxMTEgNS4yMTc0QzMuNzY0ODYgNS4xNDYxNiA0LjE1NDg0IDQuMDIzNzEgNC4xNTQ4NCA0LjAyMzcxQzQuMTU0ODQgNC4wMjM3MSA0Ljg0NzMxIDUuMDE4NjYgNC43MzczMSA2LjE3NDg1QzQuNjk5ODEgNi41NzQ4MyA0LjYzMzU3IDYuOTk4NTYgNC41NzczMiA3LjMzOTc5QzQuMzkzNTggOC40NjIyMyA0LjI1ODU5IDkuMjE4NDQgNC4yNTg1OSA5LjIxODQ0TDQuNjQ5ODIgMTAuNjIyMVYxMC45MjIxWiIgZmlsbD0iI0YxRURFQyIvPgo8cGF0aCBkPSJNMy42MDg2MiAxMy44OTgzQzMuNjA4NjIgMTMuOTUyIDMuNDQ0ODggMTQuMjMyIDMuMTAyNCAxNC42OTJDMi43NTk5MiAxNS4xNTE5IDIuNjM3NDIgMTUuMzAxOSAyLjUwODY4IDE1LjExOTRDMi4zNzk5NCAxNC45MzY5IDIuMzMyNDQgMTQuNjI3IDIuMjgzNjkgMTQuNDYzMkMyLjIzOTk0IDE0LjMxNTcgMi4xNjYyIDE0LjA3ODIgMi4xNjYyIDE0LjA3ODJMMy4yNTIzOSAxMy4zMzJDMy4yNTM2NCAxMy4zMzIgMy42MDg2MiAxMy43ODA4IDMuNjA4NjIgMTMuODk4M1oiIGZpbGw9IiM1RTYzNjciLz4KPHBhdGggZD0iTTIuNzkzNjQgNS4yNTYxMUMyLjczNzM5IDUuMjgxMSAyLjY4NzQgNS41Mjk4NCAyLjg4MjM5IDUuNzc0ODNDMy4wNzczOCA2LjAxOTgxIDMuMzAxMTEgNi4xMzczMSAzLjMwMTExIDYuMTM3MzFMMy40NzM2MSA1Ljc1ODU4QzMuNDczNjEgNS43NTg1OCAzLjIzNDg3IDUuNjA4NTkgMy4wOTk4NyA1LjQ4NDg0QzIuOTYxMTMgNS4zNTYxIDIuODQzNjQgNS4yMzM2MSAyLjc5MzY0IDUuMjU2MTFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMi4xNjk5NSAxLjU2OTkyQzIuMTM0OTYgMS41ODYxNyAxLjkyNjIyIDEuODU2MTYgMi4wNjk5NiAyLjQwMjM4QzIuMTU0OTUgMi43MjYxMSAyLjc2MjQyIDMuMzI2MDggMi43NjI0MiAzLjMyNjA4TDMuMTI0OSAzLjU3OTgyQzMuMTI0OSAzLjU3OTgyIDMuMDcxMTYgMy4yMDIzNCAyLjg1NDkyIDIuOTQ4NkMyLjYzODY4IDIuNjk0ODYgMi4zODQ5NCAyLjQwOTg4IDIuMzE2MiAyLjE3MTE0QzIuMjQ3NDUgMS45MzI0MSAyLjI4NjIgMS41MTYxOCAyLjE2OTk1IDEuNTY5OTJaIiBmaWxsPSIjQUNBMzk5Ii8+CjxwYXRoIGQ9Ik0xLjkyNDk4IDYuMTA3MzdDMS45MjQ5OCA2LjEwNzM3IDEuNzEzNzQgNi40ODExIDEuNzIyNDkgNi44NDQ4M0MxLjcyOTk5IDcuMTc3MzEgMS44MzYyNCA3LjM1NjA1IDIuMDQxMjMgNy41MzcyOUMyLjI0NDk3IDcuNzE3MjggMi41OTc0NSA3Ljg0ODUzIDIuODE3NDQgNy43NTk3OEMyLjg0NzQzIDcuNzQ3MjggMi44NTk5MyA3LjUzNzI5IDIuOTMxMTggNy4zODEwNUMyLjk4NjE4IDcuMjYyMzEgMy4xNzc0MiA3LjA3MTA3IDMuMTc3NDIgNy4wNzEwN0MzLjE3NzQyIDcuMDcxMDcgMy4yNjk5MSA3LjI3MTA2IDMuMjg0OTEgNy4zNzEwNUMzLjI5OTkxIDcuNDcxMDUgMy4zMDk5MSA3LjY2OTc5IDMuMzQ2MTYgNy42Nzg1NEMzLjQ4NDkgNy43MTM1MyAzLjY3NjE0IDcuNTc0NzkgMy43OTczOCA3LjI3NDgxQzMuOTI3MzggNi45NTM1NyAzLjkwMTEzIDYuNzY2MDggMy45MDExMyA2Ljc2NjA4TDQuMTA4NjIgNi4yOTExMUM0LjEwODYyIDYuMjkxMTEgNC4wMDExMiA1LjQ1OTkgMy4yOTI0MSA1LjI2NzQxQzIuNTgzNyA1LjA3NDkyIDEuOTYzNzMgNS40Mjk5IDEuOTYzNzMgNS40Mjk5TDEuOTI0OTggNi4xMDczN1oiIGZpbGw9InVybCgjcGFpbnQwX3JhZGlhbF83MTVfMjk1KSIvPgo8cGF0aCBkPSJNMi4yMjI0NiA2LjM5MjJDMi4wMjQ5NyA2LjM2OTcxIDEuOTQ2MjMgNi41OTA5NCAxLjk0NjIzIDYuODM3MThDMS45NDYyMyA3LjA4MzQyIDIuMDc4NzIgNy4xODU5MSAyLjIwNDk2IDcuMTg1OTFDMi4zMzEyMSA3LjE4NTkxIDIuNDQ2MiA3LjAyMzQyIDIuNDQ2MiA2Ljc5NDY4QzIuNDQ0OTUgNi41NjcyIDIuMzc4NzEgNi40MTA5NSAyLjIyMjQ2IDYuMzkyMloiIGZpbGw9IiM4NDVCNTEiLz4KPHBhdGggZD0iTTIuMzU3NDIgNC43NDM2NEMyLjM3OTkyIDQuNzk5ODkgMi40NDc0MiA0Ljg4OTg5IDIuNTQzNjYgNC45MjM2M0MyLjYzOTkxIDQuOTU3MzggMi43NTI0IDQuOTQxMTMgMi43NTI0IDQuOTQxMTNDMi43NTI0IDQuOTQxMTMgMi43ODExNSA1LjE4MzYyIDIuOTU0ODkgNS4xODk4N0MzLjEzNzM4IDUuMTk2MTIgMy4xNTIzOCA0Ljg3MzY0IDMuMTIzNjMgNC43MTYxNUMzLjA5NDg4IDQuNTU4NjUgMi45OTk4OSA0LjQ1MTE2IDIuOTk5ODkgNC40NTExNkMyLjk5OTg5IDQuNDUxMTYgMi43MTI0IDQuNDExMTYgMi41MzExNiA0LjUzNjE2QzIuMzk4NjcgNC42MjYxNSAyLjM1NzQyIDQuNzQzNjQgMi4zNTc0MiA0Ljc0MzY0WiIgZmlsbD0iIzQ2NEQ1MCIvPgo8cGF0aCBkPSJNMy4yNjQ4OCAyLjU1MzczQzMuMjY0ODggMi41NTM3MyAzLjU2NzM2IDIuNzk2MjIgMy42MDQ4NiAzLjIxNDk1QzMuNjQ2MTEgMy42ODM2NyAzLjY0NDg2IDMuNzg5OTIgMy42NDQ4NiAzLjc4OTkyTDIuNTQzNjcgMy4wOTc0NUMyLjU0MzY3IDMuMDk3NDUgMi40OTExNyAzLjIxODcgMi40ODg2NyAzLjM1MzY5QzIuNDg2MTcgMy40OTk5MyAyLjUwNzQyIDMuNjUxMTggMi41MDc0MiAzLjY1MTE4TDMuNjUxMTEgNC4zNTM2NEMzLjY1MTExIDQuMzUzNjQgMy42MDczNiA0Ljk3OTg2IDMuNDIyMzcgNS42MzQ4MkMzLjI1ODYzIDYuMjEyMjkgMy4wMzM2NCA2LjY0MjI3IDIuOTk2MTQgNi43MjIyNkMyLjk1ODY0IDYuODAyMjYgMi44Mjc0IDcuMTQ0NzQgMy4xMzg2MyA3LjIyMjI0QzMuNDExMTIgNy4yODk3MyAzLjQ2NjEyIDYuOTAxIDMuNTQ2MTEgNi43MjcyNkMzLjYyNjExIDYuNTUzNTIgMy44MTk4NSA2LjA2NDggMy45MzQ4NCA1LjYzMzU3QzQuMjQ5ODMgNC40NTM2MyA0LjE4ODU4IDMuNzQ4NjcgNC4xMTk4MyAzLjI2MTJDNC4wNDczNCAyLjczOTk3IDMuODg3MzQgMi42Mzk5OCAzLjY5NjExIDIuNTU4NzNDMy41NTg2MSAyLjUwMjQ5IDMuMzU2MTIgMi40NjQ5OSAzLjI2NDg4IDIuNTUzNzNaIiBmaWxsPSIjMzRCNkUyIi8+CjxwYXRoIGQ9Ik00LjAxNDgzIDguOTk4NDNDNC4wMTQ4MyA4Ljk5ODQzIDMuOTg2MDkgOS4zNDg0MSA0LjA2MjMzIDkuODcwODlDNC4xMjM1OCAxMC4yODQ2IDQuNDQzNTYgMTAuOTYzMyA0LjY1NDggMTAuOTEyMUM0LjcwNjA1IDEwLjg5OTYgNC41NzM1NiAxMC4yNTM0IDQuNzc3MjkgOS45OTU4OEM0LjkxNjA0IDkuODE5NjQgNS4xNjg1MiA5Ljc4ODM5IDUuODE3MjQgOS40MjA5MUM2LjM1OTcxIDkuMTEzNDMgNi44NDA5NCA4LjUxMjIxIDYuODQwOTQgOC41MTIyMUw2LjczOTY5IDcuNjI0NzZMNi4wNTU5OCA3Ljc5MjI1QzYuMDU1OTggNy43OTIyNSA1LjU1NiA4LjI3NTk3IDQuOTY5NzggOC41OTIyQzQuNDQ4NTYgOC44NzQ2OSA0LjAxNDgzIDguOTk4NDMgNC4wMTQ4MyA4Ljk5ODQzWiIgZmlsbD0iI0Q4NTNDOSIvPgo8cGF0aCBkPSJNNi40NTQ3NCA3LjU4NTk4QzYuNDUwOTkgNy41ODcyMyA2LjIxMSA3LjQ4OTc0IDYuMDU2MDEgNy43NDk3M0M1Ljg2NDc3IDguMDcwOTYgNS44NjM1MiA4LjY1MjE4IDUuODQ0NzcgOC42OTU5MkM1LjgyNjAyIDguNzM5NjcgNS43MDk3OCA4Ljg3ODQyIDUuODI2MDIgOC45MDIxNkM1Ljk0MTAxIDguOTI1OTEgNi4zODg0OSA4Ljg4NzE2IDYuNjc1OTggOC42ODA5M0M2Ljk2NDcxIDguNDc0NjkgNy4wMTcyMSA4LjE5MDk1IDcuMDE3MjEgOC4xOTA5NUw2LjQ1NDc0IDcuNTg1OThaIiBmaWxsPSIjMzRCNkUyIi8+CjxwYXRoIGQ9Ik0xMS4yNTY5IDYuNjk3NEMxMS4yNTY5IDYuNjk3NCAxMS4wODMyIDYuNjg4NjUgMTAuNjk4MiA3LjE3ODYyQzEwLjE3NTggNy44NDIzMyA5LjkwODI3IDkuMjA3MjYgOC45NzU4MiA5LjIwNzI2QzguMDQzMzcgOS4yMDcyNiA3Ljg5NDYzIDguMzk0OCA3LjY0ODM5IDcuNzY3MzRDNy40MDIxNSA3LjEzOTg3IDYuOTQ3MTggNi43OTczOSA2Ljk0NzE4IDYuNzk3MzlDNi45NDcxOCA2Ljc5NzM5IDYuNzA4NDQgNi42NDg2NSA2LjgzNTkzIDYuNDU0OTFDNi45MzM0MyA2LjMwNjE3IDcuMjUwOTEgNi40Mzk5MSA3LjQyOTY1IDYuNTIyNEM3LjYwODM5IDYuNjA0OSA3LjkzMDg4IDYuODEzNjQgOC4xMzMzNiA2Ljg5MzYzQzguNjkyMDkgNy4xMTQ4NyA5LjIwODMxIDYuOTc3MzggOS40OTk1NCA2LjkxMTEzQzkuNzkwNzggNi44NDM2NCAxMC4wNjA4IDYuNzE4NjQgMTAuNDk5NSA2LjY3MjRDMTAuOTg5NSA2LjYxODY1IDExLjI1NjkgNi42OTc0IDExLjI1NjkgNi42OTc0WiIgZmlsbD0iI0E0N0ZDQyIvPgo8cGF0aCBkPSJNNi42Njk3MiA2Ljk5ODZDNi40MjU5OSA3LjE2NzM0IDYuMTYzNSA3LjU1MzU3IDYuNDY4NDggOC4wNDcyOUM2Ljc0NzIyIDguNDk3MjcgNy4zMzk2OSA4LjM1MzUzIDcuNTA5NjggOC4xODM1M0M3LjcyMzQyIDcuOTY5OCA3LjgwODQxIDcuNTU2MDcgNy41ODU5MyA3LjE5ODU5QzcuMzYzNDQgNi44NDExMSA2Ljg5NzIxIDYuODM5ODYgNi42Njk3MiA2Ljk5ODZaIiBmaWxsPSIjRkZBNkEzIi8+CjxwYXRoIGQ9Ik02Ljk0NDcxIDcuMzIyMzFDNi44MTU5NiA3LjM1NDgxIDYuNjkzNDcgNy41Mzg1NSA2Ljc1ODQ2IDcuNzM2MDRDNi44MTQ3MSA3LjkwOTc4IDcuMDE4NDUgNy45ODEwMyA3LjE0ODQ0IDcuOTE4NTNDNy4yODU5NCA3Ljg1MjI4IDcuMzczNDMgNy42NjcyOSA3LjI5NTk0IDcuNTAyM0M3LjIzMjE5IDcuMzYzNTYgNy4wOTM0NSA3LjI4NDgxIDYuOTQ0NzEgNy4zMjIzMVoiIGZpbGw9IiNGMTgyQTciLz4KPGRlZnM+CjxyYWRpYWxHcmFkaWVudCBpZD0icGFpbnQwX3JhZGlhbF83MTVfMjk1IiBjeD0iMCIgY3k9IjAiIHI9IjEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0idHJhbnNsYXRlKDIuNjE0ODUgNy45NzY0OSkgc2NhbGUoMi4xODYxNikiPgo8c3RvcCBvZmZzZXQ9IjAuNjYyMyIgc3RvcC1jb2xvcj0iI0ZGQThBNiIvPgo8c3RvcCBvZmZzZXQ9IjAuOTE0MiIgc3RvcC1jb2xvcj0iI0ZGQThBNiIgc3RvcC1vcGFjaXR5PSIwIi8+CjwvcmFkaWFsR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIuOTk4NjEgNS40Njk4NUwyLjQ2NjE0IDUuNTkyMzRDMi40NjYxNCA1LjU5MjM0IDEuNzI3NDIgNi43MzIyOCAxLjU2NjE4IDcuOTQ0NzJDMS40NjQ5NCA4LjcwNTkzIDEuNDYxMTkgOS42OTk2MiAxLjUzMTE5IDEwLjE5NDZDMS42MDI0MyAxMC42ODk2IDEuODU3NDIgMTEuNzQzMyAyLjI1ODY1IDEyLjQ4NTdDMi42NDQ4OCAxMy4yMDA3IDMuMDIxMTEgMTMuNDgxOSAzLjQzMjMzIDEzLjczMDdDMy42MDg1OCAxMy44MzY5IDMuNzY2MDcgMTQuMjAwNiA0LjA0MTA1IDE0LjM3NjlDNC4yOTg1NCAxNC41NDE5IDQuNjM4NTIgMTQuNTc0NCA0LjYzODUyIDE0LjU3NDRDNC42Mzg1MiAxNC41NzQ0IDUuNTc4NDcgMTYuMDUwNSA3LjYxMjExIDE2LjMwNjhDOS45OTgyNCAxNi42MDY4IDExLjE2NTcgMTUuNTI5MyAxMS4xNjU3IDE1LjUyOTNDMTEuMTY1NyAxNS41MjkzIDEyLjEyMDYgMTUuNTI2OCAxMy4wMjE4IDE1LjE0MDZDMTQuMDMzIDE0LjcwNjkgMTQuNTczIDEzLjgwNDQgMTQuNTczIDEzLjgwNDRDMTQuNTczIDEzLjgwNDQgMTUuODM5MiAxMi43MTQ1IDE2LjI0MDQgMTEuNDA5NUMxNi41NjI5IDEwLjM2MjEgMTYuNTcxNiA4LjM3MDk0IDE2LjMyNDIgNy40MzM0OUMxNi4wNzY3IDYuNDk2MDQgMTUuNTk3OSA1LjcwMjMzIDE1LjIxOCA1LjE5MzYxQzE0LjkxNTUgNC43ODg2MyAxMS41MzgyIDIuOTkzNzMgMTEuMTMwNyAyLjgzMzczQzEwLjcyNDUgMi42NzQ5OSA2Ljk0MDkgMi40Mjc1MSA2Ljc5OTY2IDIuNTMzNzVDNi42NTg0MSAyLjY0MTI1IDIuOTk4NjEgNS40Njk4NSAyLjk5ODYxIDUuNDY5ODVaIiBmaWxsPSIjQUM1ODExIi8+CjxwYXRoIGQ9Ik00LjMxNjE0IDMuNDY4NjlDMy40MzQ5NCA0LjE4NjE1IDIuOTc4NzEgNC41NjExMyAyLjUyMTI0IDUuNDU4NThDMi4wNjM3NiA2LjM1NjAzIDIuMjExMjUgNy41NDcyMiAyLjI5Mzc1IDcuOTg3MkMyLjM3NSA4LjQyNzE4IDIuNTIyNDkgOS4yNzU4OCAyLjYwMzczIDkuNjk5NjFDMi42ODQ5OCAxMC4xMjQ2IDIuOTAxMjIgMTEuMTMyIDMuNDk5OTQgMTEuNzU1OEM0LjMzODY0IDEyLjYyOTUgNS4wOTg2IDEyLjkzMDcgNS4wOTg2IDEyLjkzMDdDNS4wOTg2IDEyLjkzMDcgNS43MTQ4MiAxMy44ODU2IDcuMjAzNDkgMTQuNDMxOUM5LjE2MDg5IDE1LjE0OTMgMTAuMDQyMSAxNC4xMDU2IDEwLjc5MjEgMTMuODI4MUMxMS41NDIgMTMuNTUwNyAxMi40Mzk1IDEzLjUzNDQgMTMuMjA2OSAxMi45OTU3QzEzLjk3MzEgMTIuNDU3IDE0LjgyMTggMTEuODA0NSAxNS4zNzY4IDEwLjQzNDZDMTUuOTMxOCA5LjA2NDY0IDE1LjgxNjggOC40NjA5MiAxNS44MzMgOC4xMTg0NEMxNS44NDkzIDcuNzc1OTYgMTUuODAwNSA2LjEyODU1IDE1LjE2NDMgNS4xMTczNUMxNC41MjgxIDQuMTA2MTUgMTMuNzYzMSAzLjQ5ODY4IDEzLjQ2ODIgMy4zMDYxOUMxMi44MTU3IDIuODgyNDcgMTAuNDg1OCAxLjg2ODc3IDkuODk5NiAxLjc1NTAzQzkuMzEyMTMgMS42NDEyOCA3LjIxNzI0IDEuODE4NzcgNi4zODYwMyAyLjE2MTI1QzUuNTU0ODMgMi41MDM3NCA0LjMxNjE0IDMuNDY4NjkgNC4zMTYxNCAzLjQ2ODY5WiIgZmlsbD0idXJsKCNwYWludDBfcmFkaWFsXzcxOF84OSkiLz4KPHBhdGggZD0iTTguMDM0NzYgOS45MzIxMkM3Ljg5NjAxIDkuODg4MzcgNy43NDk3NyA5LjY4MjEzIDcuNzE0NzcgOS4yNDA5QzcuNjY2MDIgOC42MTIxOSA4LjEyOTc1IDguNDM4NDUgOC4xMjk3NSA4LjQzODQ1QzguMTI5NzUgOC40Mzg0NSA4LjY4MjIyIDguMjkyMiA5LjA3MDk1IDguMTUzNDZDOS40MzIxOCA4LjAyNDcyIDkuNjkyMTcgNy44NTk3MyA5LjY5MjE3IDcuODU5NzNDOS42OTIxNyA3Ljg1OTczIDkuNzI3MTcgNy4xODYwMSA5Ljk1MDkgNi45ODcyN0MxMC4xNzU5IDYuNzg4NTMgMTAuNTcyMSA2LjgwNjAzIDExLjAxMzMgNi44NTcyOEMxMS40NTMzIDYuOTA4NTMgMTIuMTYyIDcuMjAyMjYgMTIuMDU4MyA3LjQ2OTc1QzExLjk1NDUgNy43MzcyMyAxMS44MzMzIDguMDA0NzIgMTEuNDEwOCA4LjI0NzIxQzEwLjk4ODMgOC40ODg0NCAxMC42ODU5IDguNzEzNDMgMTAuNDY5NiA4LjkwMzQyQzEwLjI1MzQgOS4wOTQ2NiA5Ljg1NzE2IDkuMzM0NjUgOS4yMDA5NCA5LjU3NzE0QzguNTQ0NzMgOS44MTk2MiA4LjIzNDc0IDkuOTk0NjEgOC4wMzQ3NiA5LjkzMjEyWiIgZmlsbD0iI0ZGQzg2RSIvPgo8cGF0aCBkPSJNNC4zMTg2NCA0LjMwODYyQzMuOTAzNjYgNC4yMjM2MyAzLjMxNzQ0IDUuMTIyMzMgMy4yMDg3IDUuNDE5ODFDMy4wOTg3IDUuNzE3MyAyLjk4NjIxIDYuNDQyMjYgMi45NzM3MSA2Ljc3MzQ5QzIuOTU3NDYgNy4xODQ3MiAyLjk0OTk2IDcuNTcwOTUgMy4xMjI0NSA3Ljg4NDY4QzMuMjU0OTUgOC4xMjcxNyAzLjU3NjE4IDguMjYzNDEgMy44NjQ5MSA4LjAxMzQzQzQuMTUzNjUgNy43NjM0NCA0LjY1NDg3IDcuMDc4NDggNC43ODczNiA2Ljk4MjIzQzQuOTE5ODYgNi44ODU5OSA1LjMwNzM0IDYuNzU4NDkgNS4xNTQ4NSA2LjEyODUzQzUuMDkyMzUgNS44NzEwNCA0LjQ1ODYzIDUuMzM4NTcgNC40MjczOCA1LjI2NjA3QzQuMzk3MzkgNS4xOTQ4MyA0LjU1MzYzIDQuMzU3MzcgNC4zMTg2NCA0LjMwODYyWiIgZmlsbD0iI0ZGQzg2RSIvPgo8cGF0aCBkPSJNNy4yNTIyNSAzLjE3OTkxQzcuMjY4NSAzLjQyMTE1IDcuNDY1OTkgMy44MDczOCA3LjY3OTczIDQuMDE2MTFDNy45OTk3MSA0LjMyNzM1IDkuNDY3MTMgNS4zNzk3OSA5Ljg0NzEyIDUuMzE5OEMxMC4yMjcxIDUuMjU5OCAxMC42NDA4IDQuNTc3MzMgMTAuNzYyMSA0LjM4NzM0QzEwLjg4MzMgNC4xOTczNiAxMS4xMjQ1IDMuOTI5ODcgMTEuMjE5NSAzLjgwODYzQzExLjMxNDUgMy42ODczOCAxMS41NjQ1IDMuNDExMTUgMTEuMzU4MyAzLjEzNDkxQzExLjE1MDggMi44NTg2OCAxMC42MzMzIDIuNjc3NDQgMTAuMjg4MyAyLjY1MTE5QzkuOTQzMzYgMi42MjQ5NCA5LjQwNzE0IDIuNDAxMiA4Ljk0OTY2IDIuNDE4N0M4LjQ5MjE5IDIuNDM2MiA3LjIwOTc1IDIuNTU3NDQgNy4yNTIyNSAzLjE3OTkxWiIgZmlsbD0iI0ZGQzg2RSIvPgo8cGF0aCBkPSJNMTMuMjkyIDYuMzk4NTNDMTMuMzIwOCA2LjY2MTAyIDEzLjY4OTUgNi43MTg1MSAxMy45ODMyIDYuOTA4NUMxNC4yNzcgNy4wOTg0OSAxNC40NzU3IDcuMjk3MjMgMTQuNjQ4MiA3LjI0NDc0QzE0LjgyMDcgNy4xOTM0OSAxNS4wNTU3IDYuOTE3MjUgMTUuMDAxOSA2LjQzMzUzQzE0LjkzMzIgNS44MTIzMSAxNC41NDQ0IDUuMDE3MzUgMTQuMDAwNyA1LjA0MzZDMTMuNjcyIDUuMDU5ODUgMTMuNzE3IDUuNjMxMDcgMTMuNzE3IDUuNjMxMDdDMTMuNzE3IDUuNjMxMDcgMTMuMjc0NSA2LjI0MzU0IDEzLjI5MiA2LjM5ODUzWiIgZmlsbD0iI0ZGQzg2RSIvPgo8cGF0aCBkPSJNNi4xODM2IDQuNzc4NDVDNi4xODM2IDQuNzc4NDUgNS4wMDQ5MSA0Ljc3MDk1IDUuMDI2MTYgNC44ODg0NEM1LjA0ODY2IDUuMDA1OTQgNS4xNTExNiA1LjM5MzQyIDUuNDU4NjQgNS40MDg0MkM1Ljc2NjEyIDUuNDIzNDEgNi4xNjIzNSA1LjQ1MjE2IDYuMzUyMzQgNS4zOTM0MkM2LjU0MjMzIDUuMzM0NjcgNy4wNjk4MSA0LjkwOTY5IDcuMDY5ODEgNC43MTk3QzcuMDY5ODEgNC41Mjk3MSA2Ljg1NzMyIDQuMjUwOTggNi44NTczMiA0LjI1MDk4TDYuMTgzNiA0Ljc3ODQ1WiIgZmlsbD0iI0NGNzAxRSIvPgo8cGF0aCBkPSJNNy44NjcxNSA2LjkzOTY4QzcuODY3MTUgNi45Mzk2OCA3Ljg1MzQgNi45NDg0MiA3LjgzMzQgNi45NjQ2N0M3Ljc3NDY2IDcuMDEzNDIgNy42NjIxNiA3LjEyNzE3IDcuNzA1OTEgNy4yNTQ2NkM3Ljc2NDY2IDcuNDIzNCA4LjEwODM5IDcuNzM4MzggOC40NDU4NyA3LjczODM4QzguNzgzMzUgNy43MzgzOCA4Ljk2NTg0IDcuNDIzNCA5LjAyNDU5IDcuMjE4NDFDOS4wODMzNCA3LjAxMzQyIDkuMDUzMzQgNi44ODg0MyA5LjA1MzM0IDYuODg4NDNMNy44NjcxNSA2LjkzOTY4WiIgZmlsbD0iI0NGNzAxRSIvPgo8cGF0aCBkPSJNNS4yMzg3MSA3Ljg5OTY5QzUuMjM4NzEgNy44OTk2OSA1LjAzMzcyIDcuOTczNDQgNS4wNDEyMiA4LjEwNDY4QzUuMDQ4NzIgOC4yMzU5MyA1LjA5OTk2IDguNDc4NDEgNS4zNjM3IDguNzM0NjVDNS42Mjk5NCA4Ljk5MzM5IDYuMDE2MTYgOS4wNjQ2MyA2LjM2NzQgOC45Njk2NEM2LjcxODYzIDguODc0NjQgNi45NzQ4NiA4LjYxMDkxIDYuOTQ2MTIgOC4yNTk2N0M2LjkxNzM3IDcuOTA4NDQgNi41Nzk4OSA3LjYzNzIxIDYuNTc5ODkgNy42MzcyMUw1LjIzODcxIDcuODk5NjlaIiBmaWxsPSIjQ0Y3MDFFIi8+CjxwYXRoIGQ9Ik0zLjcyMjU0IDkuODQwOUMzLjcyMjU0IDkuODQwOSAzLjUwMjU1IDkuODkyMTUgMy41MjUwNSAxMC4wMzg0QzMuNTQ3NTUgMTAuMTg0NiAzLjc5NjI5IDEwLjY0NTkgNC4xOTEyNyAxMC41OTQ2QzQuNTg2MjUgMTAuNTQzNCA1LjIwOTk2IDEwLjA4OTYgNS4yMDk5NiA5LjkxMzRDNS4yMDk5NiA5LjczNzE2IDQuODk0OTggOS41MDM0MiA0Ljg5NDk4IDkuNTAzNDJMMy43MjI1NCA5Ljg0MDlaIiBmaWxsPSIjQ0Y3MDFFIi8+CjxwYXRoIGQ9Ik02LjQxNzE3IDExLjY3MTlDNi40MTcxNyAxMS42NzE5IDYuMzM1OTIgMTIuMTQ0NCA2Ljk3MzM5IDEyLjMxNjhDOC4wMzU4NCAxMi42MDMxIDguMTQ1ODMgMTEuOTU4MSA4LjE0NTgzIDExLjk1ODFMNi40MTcxNyAxMS42NzE5WiIgZmlsbD0iI0NGNzAxRSIvPgo8cGF0aCBkPSJNMTAuNTkwOCAxMS44OTA2QzEwLjU5MDggMTEuODkwNiAxMC4zMDU4IDEyLjE3NTYgMTAuNjI5NSAxMi4zOTkzQzEwLjk1MzIgMTIuNjIzMSAxMS4zMjIgMTIuNjk5MyAxMS42Njk1IDEyLjY2MDZDMTIuMDE2OSAxMi42MjE4IDEyLjI1NDQgMTIuMzQ0NCAxMi4yNTQ0IDEyLjM0NDRMMTAuNTkwOCAxMS44OTA2WiIgZmlsbD0iI0NGNzAxRSIvPgo8cGF0aCBkPSJNMTMuMjUwNyA4LjcyNTgzQzEzLjI1MDcgOC43MjU4MyAxMi43MTMzIDguOTk3MDcgMTMuMTI5NSA5LjQ2NTc5QzEzLjQ0NTcgOS44MjMyNyAxNC4xNjk0IDkuNzUwNzggMTQuNDY1NyA5LjU0OTU0QzE0LjY1OTQgOS40MTgyOSAxNC43MTY5IDkuMTg1ODEgMTQuNjQ0NCA5LjA2ODMxQzE0LjU3MDcgOC45NTA4MiAxMy4yNTA3IDguNzI1ODMgMTMuMjUwNyA4LjcyNTgzWiIgZmlsbD0iI0NGNzAxRSIvPgo8cGF0aCBkPSJNMTEuMTcwOCA1LjYyMTAzQzExLjE3MDggNS42MjEwMyAxMS4xNDIgNi4yMzQ3NCAxMS45Njk1IDYuMjE0NzVDMTMuMTI2OSA2LjE4NiAxMi45ODA3IDUuNDY3MjkgMTIuOTgwNyA1LjQ2NzI5TDExLjE3MDggNS42MjEwM1oiIGZpbGw9IiNDRjcwMUUiLz4KPHBhdGggZD0iTTExLjcwNTggNC44NTIyOEMxMS43MDU4IDQuODUyMjggMTEuMTYzMyA1LjE4OTc2IDExLjEwNDYgNS4zMjg1MUMxMS4wNDU5IDUuNDY3MjUgMTAuOTIwOSA1LjkwNzIyIDExLjU1MDggNS44Nzg0OEMxMi4xODA4IDUuODQ5NzMgMTIuODEwOCA1LjgyNzIzIDEzLjAyMzIgNS41NzA5OUMxMy4yMzU3IDUuMzE0NzYgMTMuMTg0NSA1LjA3OTc3IDEzLjA5NyA0Ljg4OTc4QzEzLjAwOTUgNC42OTk3OSAxMi41MTcgMy45ODEwOCAxMi4xMjU4IDMuOTg5ODNDMTEuODY5NiAzLjk5NDgzIDExLjcwNTggNC44NTIyOCAxMS43MDU4IDQuODUyMjhaIiBmaWxsPSIjNTkzMzI5Ii8+CjxwYXRoIGQ9Ik0xMi4xMjMzIDMuOTg3NDRDMTIuMTIzMyAzLjk4NzQ0IDExLjc3OTUgMy45NDM2OSAxMS40NzgzIDQuNDc4NjZDMTEuMTc4MyA1LjAxMzYzIDExLjEwMDggNS4zMzIzNyAxMS4xMDA4IDUuMzMyMzdDMTEuMTAwOCA1LjMzMjM3IDExLjcxMiA1LjMzNDg3IDEyLjA3ODMgNS4yOTExMkMxMi40NDQ1IDUuMjQ3MzcgMTIuNjkzMiA1LjAxOTg4IDEyLjY5MzIgNS4wMTk4OEMxMi42OTMyIDUuMDE5ODggMTIuMzk0NSA0LjA1MzY4IDEyLjEyMzMgMy45ODc0NFoiIGZpbGw9IiM5MjU4NDkiLz4KPHBhdGggZD0iTTcuOTYzMzUgNi40ODU5M0M3Ljk2MzM1IDYuNDg1OTMgNy42NjMzNiA2LjQ2NTkzIDcuNjQwODcgNi41ODIxN0M3LjYxODM3IDYuNjk5NjcgNy42ODQ2MSA2Ljk5ODQgNy43ODcxMSA3LjEyOTY1QzcuODg5NiA3LjI2MDg5IDguMTk3MDkgNy41NTQ2MiA4LjUwNDU3IDcuNDk1ODhDOC44MTIwNSA3LjQzNzEzIDguOTk0NTQgNy4xMzU5IDkuMDQ1NzkgNy4wMzIxNUM5LjA5MjA0IDYuOTM0NjYgOS4xNDIwNCA2LjgyOTY2IDkuMDU0NTQgNi42OTg0MkM4Ljk2NTggNi41NjU5MyA3Ljk2MzM1IDYuNDg1OTMgNy45NjMzNSA2LjQ4NTkzWiIgZmlsbD0iIzU5MzMyOSIvPgo8cGF0aCBkPSJNOC40MDk3MSA2LjAxNzIzQzguMTU0NzMgNS45ODk3MyA3LjUzOTc2IDYuNDc4NDUgNy42NDk3NSA2LjY0NTk1QzcuNzcyMjUgNi44Mjk2OSA3Ljc5MzUgNi43MTM0NCA4LjEzMDk4IDYuODgyMThDOC40MDk3MSA3LjAyMDkzIDguNTMwOTYgNy4xNDk2NyA4LjcwNTk1IDcuMTI3MTdDOC44ODA5NCA3LjEwNDY3IDkuMDg5NjggNi44MTU5NCA5LjA1MzQzIDYuNjk4NDRDOS4wMTcxOCA2LjU4MDk1IDguNzU0NyA2LjA1MzQ4IDguNDA5NzEgNi4wMTcyM1oiIGZpbGw9IiM5MjU4NDkiLz4KPHBhdGggZD0iTTUuNTIzNjYgNC4wMzg2N0w0LjkyMzcgNC40NTM2NEM0LjkyMzcgNC40NTM2NCA0LjkwMTIgNC43MzQ4OCA1LjAyNjE5IDQuODg4NjJDNS4xNTExOCA1LjA0MjM2IDUuMTg3NDMgNS4xMzczNiA1Ljc3MzY1IDUuMTM3MzZDNi4zNTk4NyA1LjEzNzM2IDYuOTA4NTkgNS4wNTczNiA3LjAxMTA5IDQuNjM5ODhDNy4xMTM1OCA0LjIyMjQxIDYuOTMzNTkgMy45NTQ5MiA2LjgyMzYgMy44MDExOEM2LjcxMzYgMy42NDc0NCA1LjUyMzY2IDQuMDM4NjcgNS41MjM2NiA0LjAzODY3WiIgZmlsbD0iIzU5MzMyOSIvPgo8cGF0aCBkPSJNNS41MzEwNCAzLjM4NzQ1QzUuMzk2MDQgMy40NDQ5NCA1LjA3NzMxIDMuODc4NjcgNS4wMDM1NyA0LjA2ODY2QzQuOTI5ODIgNC4yNTg2NSA0LjkxMzU3IDQuNDYxMTQgNC45MjM1NyA0LjQ2MzY0QzUuMDQ0ODEgNC40OTM2NCA1Ljc1ODUzIDQuNTU5ODkgNi4wNzM1MSA0LjUwODY0QzYuMzg4NDkgNC40NTczOSA2LjY0NDczIDQuMzE4NjUgNi43NDcyMiA0LjEzNDkxQzYuODI4NDcgMy45ODg2NyA2LjgyMjIyIDMuNzk0OTMgNi44MjIyMiAzLjc5NDkzQzYuODIyMjIgMy43OTQ5MyA2LjYwODQ4IDMuNDA5OTUgNi4zMTQ3NSAzLjI5MjQ1QzYuMDIyMjYgMy4xNzQ5NiA1LjU4MjI4IDMuMzY0OTUgNS41MzEwNCAzLjM4NzQ1WiIgZmlsbD0iIzkyNTk0OSIvPgo8cGF0aCBkPSJNNS4wMjg1MyA3LjkyNzIxQzUuMDI4NTMgNy45MjcyMSA0Ljk3ODUzIDguMDAzNDYgNS4wNzM1MyA4LjE0OTdDNS4xNjg1MiA4LjI5NTk0IDUuMzU0NzYgOC40NzA5MyA1LjYzMzUgOC41NTIxOEM1LjkxMjIzIDguNjMyMTggNi4xMDk3MiA4Ljc0MjE3IDYuMjkyMjEgOC43MjA5MkM2LjQ3NTk1IDguNjk4NDIgNi44MzQ2OCA4LjU1MjE4IDYuODQ4NDMgOC4wNDcyMUM2Ljg2MzQzIDcuNTQyMjMgNi43NjA5NCA3LjMwNzI1IDYuNTQwOTUgNy4wODcyNkM2LjMyMDk2IDYuODY3MjcgNi4xNjIyMiA2LjkwNDc3IDYuMTYyMjIgNi45MDQ3N0w1LjAyODUzIDcuOTI3MjFaIiBmaWxsPSIjNTkzMzI5Ii8+CjxwYXRoIGQ9Ik01LjA5MjQ2IDcuNDU5NTZDNS4wMTQ5NiA3LjU4OTU1IDQuOTU3NDYgOC4wMTIwMyA1LjA1MjQ2IDguMTA3MDJDNS4xNDc0NSA4LjIwMjAyIDUuNzE4NjcgOC4yMDgyNyA1Ljg2ODY2IDguMTkyMDJDNi4wMTk5MSA4LjE3NTc3IDYuNTEzNjMgNy45NDMyOCA2LjUyMTEzIDcuNzY3MDRDNi41Mjg2MyA3LjU5MDggNi40MzM2MyA3LjM1NzA2IDYuNDMzNjMgNy4yNjk1N0M2LjQzMzYzIDcuMTgyMDcgNi41NDM2MyA2Ljk3NzA4IDYuMjQzNjQgNi45MDMzNEM1Ljk0MzY2IDYuODI5NTkgNS4yNTI0NSA3LjE4ODMyIDUuMDkyNDYgNy40NTk1NloiIGZpbGw9IiM5MjU4NDkiLz4KPHBhdGggZD0iTTQuMTAyNCA5LjM1N0M0LjEwMjQgOS4zNTcgMy42MzExNyA5LjQ3MzI0IDMuNjAxMTggOS42MzQ0OUMzLjU3MjQzIDkuNzk1NzMgMy42Mzk5MiA5Ljk1ODIyIDMuODAxMTYgMTAuMTEyQzMuOTYyNDEgMTAuMjY1NyA0LjA3OTkgMTAuNDI2OSA0LjIwMzY0IDEwLjQxMTlDNC4zMjg2NCAxMC4zOTY5IDQuNjU3MzcgMTAuMTA0NSA0Ljc1OTg2IDEwLjAwOTVDNC44NjIzNiA5LjkxNDQ3IDUuMDA4NiA5Ljc0NTczIDQuOTk0ODUgOS42MDY5OUM0Ljk4MTEgOS40NjgyNCA0LjkyMTExIDkuMzI4MjUgNC43ODk4NiA5LjIxODI2QzQuNjU5ODcgOS4xMDgyNiA0LjEwMjQgOS4zNTcgNC4xMDI0IDkuMzU3WiIgZmlsbD0iIzU5MzMyOSIvPgo8cGF0aCBkPSJNNC4yMTk5IDguOTM5NkMzLjk5OTkxIDguOTQ0NiAzLjgzMTE3IDkuMjEwODMgMy43NTg2NyA5LjMwNTgzQzMuNjg0OTIgOS40MDA4MiAzLjU2MTE4IDkuNjI4MzEgMy42MTI0MyA5LjY2NDU2QzMuNjYzNjggOS43MDA4MSAzLjg3NjE2IDkuNzIzMzEgMy45Nzg2NiA5Ljc4OTU1QzQuMDgxMTUgOS44NTU4IDQuMjM0OSA5Ljk1ODI5IDQuMzUyMzkgOS44NjMzQzQuNDY5ODggOS43NjgzIDQuNTcyMzggOS41MDQ1NyA0LjYzMTEyIDkuNDE3MDdDNC42ODk4NyA5LjMyOTU4IDQuNzkyMzcgOS4yMTk1OCA0Ljc5MjM3IDkuMjE5NThDNC43OTIzNyA5LjIxOTU4IDQuNTcxMTMgOC45MzIxIDQuMjE5OSA4LjkzOTZaIiBmaWxsPSIjOTI1ODQ5Ii8+CjxwYXRoIGQ9Ik02LjkwODQgMTAuNzA0NEM2Ljg2NDY1IDEwLjc1NTYgNi4yNDIxOSAxMS4yNjA2IDYuMjQyMTkgMTEuMjYwNkM2LjI0MjE5IDExLjI2MDYgNi4yNjQ2OSAxMS43MDA2IDYuNDAzNDMgMTEuODAzMUM2LjU0MjE3IDExLjkwNTYgNi45NzQ2NSAxMi4wNTE4IDcuMjUzMzggMTIuMTEwNkM3LjUzMjEyIDEyLjE2OTMgOC4wMjk1OSAxMi4yMzU2IDguMTU0NTkgMTIuMTg0M0M4LjI3OTU4IDEyLjEzMzEgOC40MTA4MiAxMS44NjE4IDguNDE4MzIgMTEuNTEwNkM4LjQyNTgyIDExLjE1OTQgOC4zMDA4MyAxMC44OTY5IDguMTYyMDkgMTAuNzM1NkM4LjAyMzM0IDEwLjU3NDQgNy45MTIxIDEwLjUxNDQgNy45MTIxIDEwLjUxNDRMNi45MDg0IDEwLjcwNDRaIiBmaWxsPSIjNTkzMzI5Ii8+CjxwYXRoIGQ9Ik03LjczNTg4IDExLjM2MzNDNy44MTcxMiAxMS4yNzIgNy45MTIxMiAxMC41MTMzIDcuOTEyMTIgMTAuNTEzM0M3LjkxMjEyIDEwLjUxMzMgNy4zNDg0IDEwLjA4MDggNy4xMTM0MSAxMC4wNTIxQzYuODc4NDIgMTAuMDIzMyA2LjYwMDk0IDEwLjA0NDYgNi41MTk2OSAxMC4xMzIxQzYuNDM5NjkgMTAuMjE5NiA2LjEyNDcxIDEwLjUzNDYgNi4xNTM0NiAxMC43Mzk1QzYuMTgyMjEgMTAuOTQ0NSA2LjI0MDk1IDExLjI1OTUgNi4yNDA5NSAxMS4yNTk1QzYuMjQwOTUgMTEuMjU5NSA3LjU2NzEzIDExLjU1NDUgNy43MzU4OCAxMS4zNjMzWiIgZmlsbD0iIzkyNTg0OSIvPgo8cGF0aCBkPSJNMTAuNDY4NCAxMS43MjQ0QzEwLjQ2ODQgMTEuNzI0NCAxMC4zOTcxIDEyLjE5OTQgMTAuOTE5NiAxMi4yODk0QzExLjQ0MjEgMTIuMzc4MSAxMS44NzU4IDEyLjQ5NjkgMTIuMTAyIDEyLjQ0OTRDMTIuMzI4MyAxMi40MDE5IDEyLjU1MzIgMTIuMDk5NCAxMi41NzcgMTEuODc5NEMxMi42MDA3IDExLjY1OTQgMTIuNjEzMiAxMS4zNDE5IDEyLjU3NyAxMS4yNTU3QzEyLjQ4MiAxMS4wMjcgMTIuMDQyIDEwLjQwMDcgMTEuODI4MyAxMC4zMzQ1QzExLjcwODMgMTAuMjk4MiAxMS41OTQ2IDEwLjMxODIgMTEuNTk0NiAxMC4zMTgyTDEwLjQ2ODQgMTEuNzI0NFoiIGZpbGw9IiM1OTMzMjkiLz4KPHBhdGggZD0iTTEwLjgwNyAxMC44OTJDMTAuNTczMiAxMS4xNTA3IDEwLjQ1MzIgMTEuNTI0NSAxMC40NjU3IDExLjcyMkMxMC40NzgyIDExLjkwODIgMTEuMzY0NCAxMS44MjcgMTEuNDY5NCAxMS43NjMyQzExLjU3NDQgMTEuNjk5NSAxMS44MDMyIDExLjQxNDUgMTEuODEwNyAxMS4zMTk1QzExLjgxODIgMTEuMjI0NSAxMS43NDU3IDEwLjkyNyAxMS43MzMyIDEwLjc0MzNDMTEuNzIwNyAxMC41NTk1IDExLjcxNTcgMTAuMzA5NSAxMS42MTQ0IDEwLjMxNThDMTEuNTEzMiAxMC4zMjIgMTEuMTM5NCAxMC41MjQ1IDEwLjgwNyAxMC44OTJaIiBmaWxsPSIjOTI1ODQ5Ii8+CjxwYXRoIGQ9Ik0xMy44MDQzIDcuNjY3MjRDMTMuODA0MyA3LjY2NzI0IDE0LjAxMDUgNy42OTcyMyAxNC4yNjQzIDcuODQ5NzNDMTQuNTYwNSA4LjAyNzIyIDE0Ljg3MDUgOC40Nzk2OSAxNC44NDY3IDguODQ4NDJDMTQuODIzIDkuMjE3MTUgMTQuNTAxNyA5LjM4MzQgMTQuMTEwNSA5LjM4OTY1QzEzLjcxOTMgOS4zOTU5IDEzLjMzNjggOS4zNzQ2NSAxMy4yNjU2IDkuMTcyMTZDMTMuMTk0MyA4Ljk2OTY3IDEzLjgwNDMgNy42NjcyNCAxMy44MDQzIDcuNjY3MjRaIiBmaWxsPSIjNTkzMzI5Ii8+CjxwYXRoIGQ9Ik0xMy40OTE5IDcuNzkwODVDMTMuNDMwNiA3Ljg3NTg0IDEzLjI3MTkgOC4xMzU4MyAxMy4yMTgxIDguNDY4MzFDMTMuMTY0NCA4LjgwMDc5IDEzLjIwMDYgOS4xODA3NyAxMy4zMTk0IDkuMjQ3MDJDMTMuNDM4MSA5LjMxMjAyIDE0LjA5MTggOC43NDgzIDE0LjEyNjggOC42ODgzQzE0LjE2MzEgOC42MjgzIDEzLjkxODEgNy43MTk2IDEzLjg0MDYgNy42NzgzNUMxMy43NjMxIDcuNjM3MTEgMTMuNTY5NCA3LjY4NDYgMTMuNDkxOSA3Ljc5MDg1WiIgZmlsbD0iIzkyNTg0OSIvPgo8ZGVmcz4KPHJhZGlhbEdyYWRpZW50IGlkPSJwYWludDBfcmFkaWFsXzcxOF84OSIgY3g9IjAiIGN5PSIwIiByPSIxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgZ3JhZGllbnRUcmFuc2Zvcm09InRyYW5zbGF0ZSg5LjAyMDE5IDguMTk5MjQpIHNjYWxlKDYuNjQ5OSA2LjY0OTkpIj4KPHN0b3Agb2Zmc2V0PSIwLjUwNyIgc3RvcC1jb2xvcj0iI0YyOUY1OCIvPgo8c3RvcCBvZmZzZXQ9IjAuNzE1MSIgc3RvcC1jb2xvcj0iI0YwOUQ1NiIvPgo8c3RvcCBvZmZzZXQ9IjAuODI2MiIgc3RvcC1jb2xvcj0iI0VCOTU1MCIvPgo8c3RvcCBvZmZzZXQ9IjAuOTE0MSIgc3RvcC1jb2xvcj0iI0UxODc0NSIvPgo8c3RvcCBvZmZzZXQ9IjAuOTg5OCIgc3RvcC1jb2xvcj0iI0Q0NzQzNiIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNEMjcxMzMiLz4KPC9yYWRpYWxHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4K', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwLjk3MzIgMTAuMTQ3MUw3LjU2MjA5IDEwLjAxNzFMNi45NTMzNyAxNS44NjE4QzYuOTUzMzcgMTUuODYxOCA3Ljg4ODMyIDE1Ljc5NjggOS4xMjU3NSAxNS43OTY4QzEwLjM2MzIgMTUuNzk2OCAxMS40MDY5IDE1Ljg4NDMgMTEuNDA2OSAxNS44ODQzTDEwLjk3MzIgMTAuMTQ3MVoiIGZpbGw9IiM4QTJFMDgiLz4KPHBhdGggZD0iTTQuMTEyMjYgMTAuOTY0NUwzLjI3NzMxIDEwLjc5MDhDMy4yNzczMSAxMC43OTA4IDMuMDcyMzIgMTIuMTE0NSAyLjg4MzU4IDEyLjkzNDRDMi42OTQ4NCAxMy43NTQ0IDIuMzgxMSAxNC44NjMxIDIuMzgxMSAxNC44NjMxTDQuMzY2IDE1Ljg0OTNDNC4zNjYgMTUuODQ5MyA2LjE2MjE1IDE2LjAwNjcgNi4xOTM0IDE1Ljk0NDJDNi4yMjQ2NSAxNS44ODE4IDcuNTQ4MzMgMTIuODA4MiA3LjU0ODMzIDEyLjgwODJMNy44NjMzMSAxMS42MjU3TDQuMTEyMjYgMTAuOTY0NVoiIGZpbGw9IiNGRjYxMTAiLz4KPHBhdGggZD0iTTEwLjMyMDcgMTEuNjEwOEwxNC40ODA1IDEwLjc0NDZMMTQuNjY5MiAxMS4yNDg0QzE0LjY2OTIgMTEuMjQ4NCAxNC45NDI5IDEyLjM5MzMgMTUuMDk0MiAxMy4wMTMzQzE1LjMzNzkgMTQuMDA4MiAxNS42NTE2IDE0Ljg5OTQgMTUuNjUxNiAxNC44OTk0TDEzLjc4NTUgMTYuMDU0M0wxMS44NjMxIDE1Ljk5MTlMMTAuMzIwNyAxMS42MTA4WiIgZmlsbD0iI0ZGNjExMCIvPgo8cGF0aCBkPSJNMy4yNjczMyAxMC4yOTcxQzMuMjY3MzMgMTAuMzI1OSAzLjI4NzMzIDEwLjUzNDYgMy4yNzczMyAxMC42ODcxQzMuMjY3MzMgMTAuODM5NiAzLjIzODU5IDExLjAyNzEgMy4yMzg1OSAxMS4wMjcxQzMuMjM4NTkgMTEuMDI3MSAzLjM1MjMzIDExLjA1MjEgMy40NTczMiAxMS4xMTk2QzMuNTYyMzIgMTEuMTg3MSAzLjc3MjMxIDExLjQ0NDYgMy43NzIzMSAxMS40NDQ2QzMuNzcyMzEgMTEuNDQ0NiA0LjE4MjI5IDExLjI5MzMgNC40MzEwMiAxMS4zODcxQzQuODEzNSAxMS41MzA4IDQuOTc1OTkgMTEuODg0NSA0Ljk3NTk5IDExLjg4NDVDNC45NzU5OSAxMS44ODQ1IDUuMzY3MjIgMTEuNTIwOCA1Ljg0NTk1IDExLjU5ODNDNi4zMTM0MiAxMS42NzMzIDYuNDk1OTEgMTIuMTE0NSA2LjQ5NTkxIDEyLjExNDVDNi40OTU5MSAxMi4xMTQ1IDYuODQ3MTUgMTEuODMwOCA3LjE1NDYzIDExLjg0N0M3LjUxODM2IDExLjg2NTggNy42NTIxIDEyLjEzMzMgNy42NTIxIDEyLjEzMzNMOC4xOTcwOCAxMS4wMzQ2TDQuMTQ2MDQgMTAuMjk4NEgzLjI2NzMzVjEwLjI5NzFaIiBmaWxsPSIjQUYwRDAzIi8+CjxwYXRoIGQ9Ik0yLjE3NzM3IDE1LjM4MDdMMi4zOTczNiAxNC43OTgyQzIuMzk3MzYgMTQuNzk4MiAzLjM3MjMxIDE1LjI3NTcgNC4wNzg1MiAxNS40ODU3QzQuNzg0NzMgMTUuNjk1NiA1LjYxNzE5IDE1LjcyNDQgNS42MTcxOSAxNS43MjQ0QzUuNjE3MTkgMTUuNzI0NCA1LjYwNzE5IDE1LjM5NDQgNS4zMDA5NSAxNS4xNTU3QzQuOTk0NzIgMTQuOTE2OSA0LjUwOTc1IDE0LjUxNTcgNC41MDk3NSAxNC41MTU3TDUuMTcyMjEgMTQuMjk0NUw1LjU0MDk0IDE0LjE0N0w2LjY5NzEzIDE1LjUyMzJMNi4zMjQ2NSAxNi40NTA2QzYuMzI0NjUgMTYuNDUwNiA1LjUxMjE5IDE2LjU1NTYgNC4yMTM1MSAxNi4yMjE5QzIuOTE0ODMgMTUuODg4MSAyLjE3NzM3IDE1LjM4MDcgMi4xNzczNyAxNS4zODA3WiIgZmlsbD0iI0M5QzlDOSIvPgo8cGF0aCBkPSJNMTAuMjQxOSAxMC44NDJDMTAuMjYwNyAxMC45MTgzIDEwLjI1MTkgMTEuNTIwOCAxMC4yNTE5IDExLjUyMDhMMTAuNjA1NyAxMS45MjJDMTAuNjA1NyAxMS45MjIgMTAuNzk2OSAxMS42NzMyIDExLjIzNjkgMTEuNjkzMkMxMS42NzY5IDExLjcxMiAxMS44NzY5IDExLjkxMzIgMTEuODc2OSAxMS45MTMyQzExLjg3NjkgMTEuOTEzMiAxMi4xNDQzIDExLjU4ODMgMTIuNTA4MSAxMS41MjJDMTIuODcxOCAxMS40NTQ1IDEzLjIyNDMgMTEuNjQ1NyAxMy4yMjQzIDExLjY0NTdDMTMuMjI0MyAxMS42NDU3IDEzLjQ3MyAxMS4xODcgMTMuODM1NSAxMS4wODJDMTQuMTk5MiAxMC45NzcgMTQuNjY5MiAxMS4yNDk1IDE0LjY2OTIgMTEuMjQ5NUwxNC40NzU1IDEwLjA2OTZMMTAuOTExOSAxMC4zMjU4TDEwLjI0MTkgMTAuODQyWiIgZmlsbD0iI0FGMEQwMyIvPgo8cGF0aCBkPSJNMTUuNjEyOSAxNC43Nzk1TDE1Ljg0MTcgMTUuMzYxOUMxNS44NDE3IDE1LjM2MTkgMTUuMTQ0MiAxNS45MDY5IDE0LjI0NTUgMTYuMTU1NkMxMy4zNDY4IDE2LjQwNDQgMTIuMDk2OSAxNi41MDQ0IDEyLjA5NjkgMTYuNTA0NEwxMS43MzE5IDE0LjQ1NTdMMTIuOTY0MyAxNC4wODMzTDEzLjY0MDUgMTQuMzcyQzEzLjY0MDUgMTQuMzcyIDEzLjA0MTggMTQuODc1NyAxMi44NzgxIDE1LjEwNjlDMTIuNTY2OCAxNS41NDQ0IDEyLjY3ODEgMTUuODIxOSAxMi42NzgxIDE1LjgyMTlDMTIuNjc4MSAxNS44MjE5IDEzLjYzMyAxNS43MzU3IDE0LjM2OTIgMTUuNDIwN0MxNS4xMDY3IDE1LjEwMzIgMTUuNjEyOSAxNC43Nzk1IDE1LjYxMjkgMTQuNzc5NVoiIGZpbGw9IiNDOUM5QzkiLz4KPHBhdGggZD0iTTQuOTc3MjMgMTQuNTAxOEM0Ljk3NzIzIDE0LjUwMTggNS40MDA5NiAxNC42OTE3IDUuNjE3MiAxNC44NDU1QzUuODg0NjkgMTUuMDM2NyA2LjE1MjE3IDE1LjMyMyA2LjIzODQyIDE1Ljc4MTdDNi4yOTg0MSAxNi4wOTc5IDYuMzI0NjYgMTYuNDUwNCA2LjMyNDY2IDE2LjQ1MDRDNi4zMjQ2NiAxNi40NTA0IDYuNjIwOSAxNi42NzkxIDcuMjAzMzcgMTUuNzA1NEM3Ljc4NTg0IDE0LjczMTcgOC4wODIwNyAxMy4zODMxIDguMTMwODIgMTIuNDI4MUM4LjE3ODMxIDExLjQ3MzIgOC4xNTk1NyAxMS4yNTMyIDguMTU5NTcgMTEuMjUzMkw3LjI5OTYxIDEyLjY2NjhMNi4wMjg0MyAxMy44MjNMNC45NzcyMyAxNC41MDE4WiIgZmlsbD0iI0Q5MkYwQSIvPgo8cGF0aCBkPSJNOC4xNjcwOSAxMS4yNTgzQzguMTY3MDkgMTEuMjU4MyA4LjI2NzA4IDEyLjEyNTggNy4wOTg0IDEzLjIwMkM2LjM3MjE4IDEzLjg3MDcgNS42NjM0NyAxNC4zMzA3IDUuMTc4NSAxNC41MjA2QzUuMDEyMjYgMTQuNTg1NiA0Ljg2MzUxIDE0LjU1OTQgNC43NTg1MiAxNC41NTgxQzQuNDE2MDQgMTQuNTUzMSA0LjQ3OTc4IDE0LjQ5NDQgNC42NzEwMiAxNC4zOTY5QzQuODk2MDEgMTQuMjgxOSA1LjU0OTczIDE0LjA0MzIgNi43NzM0MSAxMi45MTU3QzcuNzUyMTEgMTIuMDEzMyA3LjgxOTYxIDExLjExMDggNy44MTk2MSAxMS4xMTA4TDguMTY1ODQgMTAuOTIzM1YxMS4yNTgzSDguMTY3MDlaIiBmaWxsPSIjRkZGRUZGIi8+CjxwYXRoIGQ9Ik0xMi4xNDQ0IDE2LjQ5OTJDMTIuMTQ0NCAxNi40OTkyIDExLjcxNDQgMTYuNjUxNyAxMS4wNjQ0IDE1Ljc3M0MxMC40MTQ1IDE0Ljg5NDMgMTAuMjE0NSAxMy4zMzY5IDEwLjE4NTcgMTIuODM5NEMxMC4xNTcgMTIuMzQyIDEwLjE2NyAxMS40MDU4IDEwLjE2NyAxMS40MDU4TDExLjM2MTkgMTIuOTA1N0MxMS4zNjE5IDEyLjkwNTcgMTMuMjE1NiAxNC4zMDA2IDEzLjE4NjggMTQuMzEwNkMxMy4xNTgxIDE0LjMyMDYgMTIuNTgxOCAxNC42ODMxIDEyLjM2NTYgMTUuMDQ2OEMxMS45MjQ0IDE1Ljc4MTggMTIuMTQ0NCAxNi40OTkyIDEyLjE0NDQgMTYuNDk5MloiIGZpbGw9IiNEOTJGMEEiLz4KPHBhdGggZD0iTTEwLjEwMDcgMTAuOTkyMUMxMC4xMDA3IDEwLjk5MjEgMTAuMDgzMiAxMS4zMTk2IDEwLjE0NyAxMS41Njg0QzEwLjI4MDcgMTIuMDkzMyAxMC43NDE5IDEyLjY2MDggMTEuMzQxOSAxMy4xOTMzQzEyLjExNTYgMTMuODgwNyAxMy4wMTQzIDE0LjM1OTUgMTMuMDgwNiAxNC4zNzgyQzEzLjE0OCAxNC4zOTcgMTMuNjI1NSAxNC40MTcgMTMuNjM0MyAxNC4zNjgyQzEzLjYzOCAxNC4zNTIgMTMuNjY2OCAxNC4yODA3IDEzLjUxMDUgMTQuMTY3QzEzLjE4OTMgMTMuOTM1NyAxMi40NzgxIDEzLjUyODMgMTEuODk1NiAxMy4wNjgzQzExLjQ4NDQgMTIuNzQzMyAxMC45NTY5IDEyLjMzOTYgMTAuNDk4MiAxMS41MDcxQzEwLjQxNDQgMTEuMzU1OSAxMC40NDQ0IDEwLjk3MzQgMTAuNDQ0NCAxMC45NzM0TDEwLjEwMDcgMTAuOTkyMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zLjc0NjA4IDguMTU5NTlDMy43NDYwOCA4LjE1OTU5IDIuNzUxMTMgOC4yODk1OCAyLjcxOTg4IDguNDMwODJDMi42ODg2MyA4LjU3MzMxIDIuNzk4NjMgMTAuNDUwNyAyLjc5ODYzIDEwLjQ1MDdDMi43OTg2MyAxMC40NTA3IDMuMjE3MzYgMTAuMzczMiAzLjQyODYgMTAuNDE5NUMzLjYzOTgzIDEwLjQ2NTcgNC4wMzczMSAxMC43MjMyIDQuMDM3MzEgMTAuNzIzMkw0LjcwMzUzIDEwLjMxMzJMNS4xODM1IDEwLjk3OTRDNS4xODM1IDEwLjk3OTQgNS41MjIyMyAxMC44MDQ0IDUuOTkwOTYgMTAuODUwN0M2LjQ1OTY5IDEwLjg5NjkgNi43NzQ2NyAxMS4yMTMyIDYuNzc0NjcgMTEuMjEzMkw3LjQyOTYzIDEwLjY3NDVMOC4xNjA4NSAxMS4yNDk0QzguMTYwODUgMTEuMjQ5NCA4LjQ5NDU4IDEwLjg2MDcgOS4xMTQ1NCAxMC44NDk0QzkuNzM0NTEgMTAuODM4MiAxMC4xMDk1IDExLjI5NDQgMTAuMTA5NSAxMS4yOTQ0TDEwLjkyODIgMTAuNTgwN0wxMS43MzU3IDExLjIwMDdDMTEuNzM1NyAxMS4yMDA3IDEyLjA1MTkgMTAuOTQzMiAxMi40MTQ0IDEwLjg2MTlDMTIuNzc2OSAxMC43Nzk0IDEzLjA0NTYgMTAuOTIwNyAxMy4wNDU2IDEwLjkyMDdMMTMuNTYwNiAxMC4wMDgyTDE0LjE0NTUgMTAuNDI5NUMxNC4xNDU1IDEwLjQyOTUgMTQuMzMzIDEwLjIwNyAxNC42MDE4IDEwLjE3MkMxNC44NzA1IDEwLjEzNyAxNS4xOTggMTAuMTI1NyAxNS4xOTggMTAuMTI1N0MxNS4xOTggMTAuMTI1NyAxNS4xNzQyIDguODYyMDUgMTUuMTc0MiA4LjY3NDU2QzE1LjE3NDIgOC40ODcwNyAxNS4xMDQyIDguMTgzMzMgMTUuMDIxNyA4LjEyNDU5QzE0LjkzOTIgOC4wNjU4NCAxNC4yMDMgNy45MDIxIDE0LjIwMyA3LjkwMjFMMy43NDYwOCA4LjE1OTU5WiIgZmlsbD0iI0Q5MkYwQSIvPgo8cGF0aCBkPSJNNS4wNjIyMSA4LjcwNTgxTDQuMDg2MDEgOC45MzIwNUw0LjAzNjAxIDEwLjcyNDVDNC4wMzYwMSAxMC43MjQ1IDQuMzMzNDkgMTAuNjM4MiA0LjYzMDk4IDEwLjY2OTVDNC45Mjg0NiAxMC43MDA3IDUuMTgyMiAxMC45ODE5IDUuMTgyMiAxMC45ODE5QzUuMTgyMiAxMC45ODE5IDUuMTc1OTUgOS44MTcgNS4xODU5NSA5LjYyMDc2QzUuMTk1OTUgOS40MjQ1MiA1LjI3MjIgOS4wNTIwNCA1LjMxMzQ0IDguOTgwOEM1LjM1NDY5IDguOTA4MyA1LjA2MjIxIDguNzA1ODEgNS4wNjIyMSA4LjcwNTgxWiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNNy42MzIwOSA5LjA4NzE2TDYuNzQwODkgOS40MTMzOUM2Ljc0MDg5IDkuNDEzMzkgNi43MTcxNCAxMC4yMzgzIDYuNzM4MzkgMTAuNTY3MUM2Ljc1ODM5IDEwLjg5NTggNi43NzMzOSAxMS4yMTU4IDYuNzczMzkgMTEuMjE1OEM2Ljc3MzM5IDExLjIxNTggNy4xNzk2MiAxMS4wODA4IDcuNTA5NiAxMS4xMDA4QzcuODM4MzMgMTEuMTIwOCA4LjE1OTU2IDExLjI1MiA4LjE1OTU2IDExLjI1Mkw4LjE0NzA3IDkuNDg3MTRMNy42MzIwOSA5LjA4NzE2WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTAuMDc5NSA5LjQ1NzAzQzEwLjA3OTUgOS40NTcwMyAxMC4xMTA3IDEwLjEyNTcgMTAuMTIwNyAxMC40NjQ1QzEwLjEzMDcgMTAuODAzMiAxMC4xMDgyIDExLjI5ODIgMTAuMTA4MiAxMS4yOTgyQzEwLjEwODIgMTEuMjk4MiAxMC41OTMyIDExLjA1MDcgMTAuOTc0NCAxMS4wMTk0QzExLjM1NDQgMTAuOTg4MiAxMS43MzQ0IDExLjIwNDQgMTEuNzM0NCAxMS4yMDQ0QzExLjczNDQgMTEuMjA0NCAxMS43MDQ0IDEwLjUwNTcgMTEuNjYzMSA5Ljk5MkMxMS42MzQ0IDkuNjMzMjcgMTEuNTI5NCA5LjMyMzI4IDExLjUyOTQgOS4zMjMyOEwxMC41NTMyIDguNzk5NTZMMTAuMDc5NSA5LjQ1NzAzWiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTMuMDQ0MyAxMC45MjMyQzEzLjA0NDMgMTAuOTIzMiAxMy4zNjgxIDEwLjU2NyAxMy42MjU1IDEwLjQ4NDVDMTMuODgzIDEwLjQwMiAxNC4xNDQzIDEwLjQzMiAxNC4xNDQzIDEwLjQzMkMxNC4xNDQzIDEwLjQzMiAxNC4xMjkzIDkuNTkwNzggMTQuMDg4IDkuMjA5NTVDMTQuMDQ2OCA4LjgyOTU3IDE0LjAwOTMgOC42MTA4MyAxNC4wMDkzIDguNjEwODNMMTIuODE0MyA4LjA5OTYxTDEyLjg0ODEgOS4wMzk1NkMxMi44NDgxIDkuMDM5NTYgMTIuOTg5MyA5LjM1MzI5IDEzLjAzMDYgOS44OTgyNkMxMy4wNzA2IDEwLjQ0MzIgMTMuMDQ0MyAxMC45MjMyIDEzLjA0NDMgMTAuOTIzMloiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTkuMDUyMDIgNC43MjcyOUw3LjYwOTU5IDUuMzY5NzZDNy42MDk1OSA1LjM2OTc2IDYuMjk4NDEgNi4zNTM0NiA1LjI2MDk3IDcuMDA5NjdDNC4yMjM1MiA3LjY2NTg5IDMuNjI3MyA3LjkxNTg4IDMuNTAzNTYgNy45NjcxMkMzLjMwNjA3IDguMDQ5NjIgMi45ODYwOSA4LjE0NzExIDIuODQ3MzQgOC4yNDIxMUMyLjYxNzM2IDguNDAyMSAyLjcxMzYgOC40NzcxIDIuNzUxMSA4LjQ4OTZDMi43ODg2IDguNTAyMSAzLjkzNjA0IDkuMDE1ODIgNS45Njk2OCA5LjMxNzA1QzguMDAzMzIgOS42MTgyOSAxMC43NjgyIDkuNDk1NzkgMTIuMTg1NiA5LjIyMDgxQzEzLjYwMTggOC45NDU4MiAxNS4wMjE3IDguMTI1ODcgMTUuMDIxNyA4LjEyNTg3QzE1LjAyMTcgOC4xMjU4NyAxNC45MDMgNy45MjcxMyAxNC4xMTY3IDcuNTg1ODlDMTMuMzI5MyA3LjI0NDY2IDEyLjY2MDYgNi44NzcxOCAxMS41MTk0IDYuMTI5NzJDMTAuMzc4MiA1LjM4MjI2IDEwLjEwMzIgNS4wMTQ3OCAxMC4xMDMyIDUuMDE0NzhMOS4wNTIwMiA0LjcyNzI5WiIgZmlsbD0iI0ZGNjExMCIvPgo8cGF0aCBkPSJNNy44MzIwNiA1LjYzMjMyQzcuODMyMDYgNS42MzIzMiA2LjI0NDY1IDcuMjU4NDkgNC4yODk3NSA4LjYwOTY3QzQuMDc2MDEgOC43NTcxNiA0LjA4NjAxIDguOTMyMTUgNC4wODYwMSA4LjkzMjE1QzQuMDg2MDEgOC45MzIxNSA0LjI0MzUgOC45OTIxNSA0LjYzMDk4IDkuMDgyMTRDNS4wMjQ3MSA5LjE3MzM5IDUuMjUwOTUgOS4xOTU4OSA1LjI1MDk1IDkuMTk1ODlDNS4yNTA5NSA5LjE5NTg5IDUuNDI5NjkgOC45OTA5IDUuODEwOTIgOC42MDk2N0M2LjQ5MzM4IDcuOTI3MiA2LjkxNzExIDcuMzg1OTggNy40NTA4MyA2LjY5NDc3QzcuODA0NTYgNi4yMzYwNCA4LjIyNTc5IDUuNjg2MDcgOC4yMjU3OSA1LjY4NjA3TDcuODMyMDYgNS42MzIzMloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik04LjUxNDU2IDUuODQyMzZDOC41MTQ1NiA1Ljg0MjM2IDcuOTM3MDkgNy4xNzk3OSA3LjY2MjExIDcuNzU3MjZDNy41MzIxMSA4LjAyODUgNy4yNDIxMyA4LjUzMDk3IDYuOTc5NjQgOC45NTA5NUM2Ljc1MDkxIDkuMzE3MTggNi43NDA5MSA5LjQxNDY3IDYuNzQwOTEgOS40MTQ2N0M2Ljc0MDkxIDkuNDE0NjcgNy4yNTQ2MyA5LjQ4ODQyIDcuNTAzMzcgOS40ODg0MkM3Ljc1MjEgOS40ODg0MiA4LjE0NTgzIDkuNDg1OTIgOC4xNDU4MyA5LjQ4NTkyQzguMTQ1ODMgOS40ODU5MiA4LjM0MzMyIDguODk3MiA4LjU1MzMxIDcuNzE3MjZDOC42NzU4IDcuMDMxMDUgOC44NTQ1NCA1LjgyODYxIDguODU0NTQgNS44Mjg2MUw4LjUxNDU2IDUuODQyMzZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOS4zMTQ1MSA1Ljg5NDc4QzkuMzE0NTEgNS44OTQ3OCA5LjQ0NDUxIDcuMTgzNDcgOS41ODk1IDcuODYyMThDOS43ODU3NCA4Ljc4MDg4IDEwLjA3ODIgOS40NTU4NSAxMC4wNzgyIDkuNDU1ODVDMTAuMDc4MiA5LjQ1NTg1IDEwLjU3MzIgOS40MzU4NSAxMC44NDgyIDkuNDA5NkMxMS4xMjMyIDkuMzgzMzUgMTEuNTI2OSA5LjMyMjEgMTEuNTI2OSA5LjMyMjFDMTEuNTI2OSA5LjMyMjEgMTEuMDU0NCA4LjU5NDY0IDEwLjU5ODIgNy44NjIxOEMxMC4yOTcgNy4zNzcyMSA5LjczMTk5IDYuMjQ4NTIgOS42NDA3NSA1LjgxNjA0QzkuNTYzMjUgNS40NDM1NiA5LjMxNDUxIDUuODk0NzggOS4zMTQ1MSA1Ljg5NDc4WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEwLjAyMiA1LjY1ODU0QzEwLjAyMiA1LjY5NzI5IDEwLjQzNyA2LjU4MjI0IDExLjQ2NDQgNy42OTIxOEMxMi4yOTA2IDguNTg0NjMgMTIuODQxOCA5LjA0MzM2IDEyLjg0MTggOS4wNDMzNkMxMi44NDE4IDkuMDQzMzYgMTMuMzEzIDguODk4MzcgMTMuNTI0MyA4LjgyMDg3QzEzLjc3MyA4LjcyOTYzIDE0LjAwOTMgOC42MTA4OCAxNC4wMDkzIDguNjEwODhDMTQuMDA5MyA4LjYxMDg4IDEyLjg5NDMgNy44MzcxNyAxMi4yMzgxIDcuMjk5N0MxMS41ODE5IDYuNzYyMjMgMTAuNjc2OSA2LjAxNDc3IDEwLjQyODIgNS42NzM1NEMxMC4xNzk1IDUuMzMxMDUgMTAuMDIyIDUuNjU4NTQgMTAuMDIyIDUuNjU4NTRaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOS4wMzgyNiAzLjgzNjIxQzguNzYyMDMgMy44NTYyMSA4LjY3MDc4IDQuMzIxMTkgOC41MjcwNCA0LjQ2NjE4QzguMzgzMyA0LjYwOTkyIDcuNjA4MzQgNS4zNzExMyA3LjYwODM0IDUuMzcxMTNDNy42MDgzNCA1LjM3MTEzIDcuNzQ1ODMgNi4wODk4NSA5LjExMDc2IDYuMDc2MUMxMC4zNTk0IDYuMDYyMzUgMTAuNTk2OSA1LjQ3MTEzIDEwLjU5NjkgNS40NzExM0MxMC41OTY5IDUuNDcxMTMgOS43OTk0NyA0LjYyMzY3IDkuNjE1NzMgNC40MTM2OEM5LjQzMTk5IDQuMjAyNDQgOS40MDU3NCAzLjgwOTk3IDkuMDM4MjYgMy44MzYyMVoiIGZpbGw9IiNEOTJGMEEiLz4KPHBhdGggZD0iTTkuMDg0NTQgMy4yMjg3OEM5LjA4NDU0IDMuMjI4NzggOS4zMjA3OCAzLjE1Mzc4IDkuNjU3MDEgMy4xOTI1M0M5Ljk5OTQ5IDMuMjMyNTMgMTAuMjI3IDMuMzQzNzcgMTAuNTgzMiAzLjMzNjI3QzExLjEyODIgMy4zMjYyNyAxMS41NjY5IDMuMDcwMDMgMTEuNzI0NCAyLjkyNTA0QzExLjg4MTkgMi43ODAwNSAxMi4wODMxIDIuNTQ1MDYgMTEuOTc4MSAyLjQ2NjMyQzExLjg3MzEgMi4zODc1NyAxMS40MjU3IDIuNTA2MzEgMTAuOTI1NyAyLjMwODgzQzEwLjQ4NDUgMi4xMzUwOCAxMC4yNjMyIDEuNjgxMzYgOS42MzcwMSAxLjYwODg2QzkuMDk4MjkgMS41NDc2MiA4Ljk0MDggMS43Njg4NSA4Ljk0MDggMS43Njg4NUw5LjA4NDU0IDMuMjI4NzhaIiBmaWxsPSIjRkY2MTEwIi8+CjxwYXRoIGQ9Ik04Ljg3NDUxIDEuNjExMzNWNC4wNjk5NUw5LjI2ODI0IDMuOTY0OTVMOS4yMjk0OSAxLjYxMTMzSDguODc0NTFaIiBmaWxsPSIjRDkyRjBBIi8+Cjwvc3ZnPgo=', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1Ljg5ODEgMTMuOTM2OEgxMC4zMTcxVjE0LjgwM0gxNS44OTgxVjEzLjkzNjhaIiBmaWxsPSIjQUYwQzFBIi8+CjxwYXRoIGQ9Ik05LjY0NTc1IDE0LjU0OTNWMTUuMjEzTDE2LjQ3OTEgMTUuMjIwNUMxNi40NzkxIDE1LjIyMDUgMTYuNDk0MSAxNC41NzgxIDE2LjQ3MTYgMTQuNTc4MUMxNi40NTA0IDE0LjU3OTMgOS42NDU3NSAxNC41NDkzIDkuNjQ1NzUgMTQuNTQ5M1oiIGZpbGw9IiNEQzBEMkEiLz4KPHBhdGggZD0iTTkuNjQ1NzUgMTMuMzg4Mkw5LjY2MDc1IDE0LjA1OTRDOS42NjA3NSAxNC4wNTk0IDE2LjQ1NzkgMTQuMDk1NiAxNi40NjU0IDE0LjA3NDRDMTYuNDcyOSAxNC4wNTMxIDE2LjQ4MDQgMTMuNDM5NCAxNi40NTA0IDEzLjQyNDRDMTYuNDIxNiAxMy40MDk0IDkuNjQ1NzUgMTMuMzg4MiA5LjY0NTc1IDEzLjM4ODJaIiBmaWxsPSIjREMwRDJBIi8+CjxwYXRoIGQ9Ik0yLjEwOTg2IDYuOTg0NzRDMi4xMDk4NiA2Ljk4NDc0IDIuMTA5ODYgOS4yNDIxMyAyLjEwOTg2IDkuMzE4MzdDMi4xMDk4NiA5LjM5NDYyIDIuMTYyMzYgOS40NjIxMSAyLjI2MTExIDkuNDY5NjFDMi4zNTk4NSA5LjQ3NzExIDMuNjU4NTMgOS40NTQ2MSAzLjczMzUzIDkuNDYyMTFDMy44MDg1MiA5LjQ2OTYxIDMuODkyMjcgOS40MDIxMiAzLjg5MjI3IDkuMjY1ODdDMy44OTIyNyA5LjEyOTYzIDMuODkyMjcgNi45OTM0OSAzLjg5MjI3IDYuOTkzNDlDMy44OTIyNyA2Ljk5MzQ5IDMuNDM5NzkgNi42NTM1MSAzLjM4NjA1IDYuNjYxMDFDMy4zMzIzIDYuNjY4NTEgMi4xMDk4NiA2Ljk4NDc0IDIuMTA5ODYgNi45ODQ3NFoiIGZpbGw9IiM4NTg1ODUiLz4KPHBhdGggZD0iTTIuNjYyMzMgNy41MjQ2NUMyLjU1NzM0IDcuNTM3MTUgMi41MTEwOSA3LjU5OTY0IDIuNTExMDkgNy43MjA4OUMyLjUxMTA5IDcuODQyMTMgMi40OTYwOSA4LjY1NzA5IDIuNDk2MDkgOC43Mzk1OEMyLjQ5NjA5IDguODIyMDggMi41MDM1OSA4LjkwNTgyIDIuNjI0ODQgOC45MTMzMkMyLjc0NjA4IDguOTIwODIgMy4yODk4IDguOTI4MzIgMy4zNTczIDguOTIwODJDMy40MjQ3OSA4LjkxMzMyIDMuNDc4NTQgOC44MzgzMyAzLjQ4NjA0IDguNzM5NThDMy40OTM1NCA4LjY0MDg0IDMuNDg2MDQgNy43NTgzOCAzLjQ4NjA0IDcuNjg5NjRDMy40ODYwNCA3LjU2ODM5IDMuNDAzNTUgNy41MzA5IDMuMjk3MyA3LjUzMDlDMy4xOTEwNiA3LjUzMDkgMi43ODIzMyA3LjUwOTY1IDIuNjYyMzMgNy41MjQ2NVoiIGZpbGw9IiNCMEIwQjAiLz4KPHBhdGggZD0iTTEzLjc0NDQgNy42NTcxTDEzLjc1OTQgNi44Nzk2NEwxMS45MzIgNi44ODcxNEwxMS45MTcgNy42MTk2TDEyLjM2OTUgOC4xMTA4MkgxMi42MzgyQzEyLjYyODIgOC44MzcwNCAxMi41OTU3IDExLjExNTcgMTIuNTk1NyAxMS4xMTU3QzEyLjU5NTcgMTEuMTE1NyAxMi40OTMyIDExLjI2MTkgMTIuMzc3IDExLjM3MTlDMTIuMjI1NyAxMS41MTU2IDExLjk3NyAxMS43MDQ0IDExLjk5OTUgMTIuMTcxOUMxMi4wMjA3IDEyLjYwMTggMTIuNDQ0NSAxMi45MjY4IDEyLjg2MDcgMTIuOTI2OEMxMy4zMDU3IDEyLjkyNjggMTMuNjA4MiAxMi42MDE4IDEzLjYyMzIgMTIuMjkzMUMxMy42MzgyIDExLjk4MzEgMTMuNTE2OSAxMS45NzA2IDEzLjQ0OTQgMTEuOTYwNkMxMy4zNDMyIDExLjk0NTYgMTMuMjYwNyAxMi4wMjA2IDEzLjIzODIgMTIuMTU2OUMxMy4yMTU3IDEyLjI5MzEgMTMuMDc5NCAxMi40NjY4IDEyLjg0NTcgMTIuNDEzMUMxMi42MTIgMTIuMzU5NCAxMi40NjA3IDEyLjEwMzEgMTIuNjQ5NSAxMS44NTQ0QzEyLjc4NDQgMTEuNjc1NiAxMy4wNDE5IDExLjYyMDYgMTMuMDY0NCAxMS40Njk0QzEzLjA4NDQgMTEuMzM5NCAxMy4wNjk0IDguNzk5NTQgMTMuMDY1NyA4LjEwOTU3SDEzLjM0NDRMMTMuNzQ0NCA3LjY1NzFaIiBmaWxsPSIjODU4NTg3Ii8+CjxwYXRoIGQ9Ik0xMS40NTY5IDEzLjI1ODJDMTEuMzEzMSAxMy4zMjQ1IDExLjMzMzEgMTMuNDM4MiAxMS4zMzQ0IDEzLjU1NDVDMTEuMzM5NCAxNC4wNjE5IDExLjM2MzEgMTUuMjE0NCAxMS4zNjMxIDE1LjIxNDRDMTEuMzYzMSAxNS4yMTQ0IDExLjM5NjkgMTUuMzgwNiAxMS42MTU2IDE1LjM4MDZDMTEuODc2OSAxNS4zODA2IDExLjg5NjkgMTUuMjA2OSAxMS44OTY5IDE1LjIwNjlWMTMuNDgyTDEyLjg4MzEgMTIuNjY0NUwxNC4xOTE3IDEzLjYyNTdDMTQuMTkxNyAxMy42MjU3IDE0LjIyOCAxNS4xMTk0IDE0LjIyOCAxNS4yMDU2QzE0LjIyOCAxNS4yOTE5IDE0LjMxNDIgMTUuNDA4MSAxNC41MTY3IDE1LjQwMDZDMTQuNzQ4IDE1LjM5MTkgMTQuNzc2NyAxNS4yMDU2IDE0Ljc3NjcgMTUuMjA1NkMxNC43NzY3IDE1LjIwNTYgMTQuNzY4IDEzLjYwNTcgMTQuNzYwNSAxMy40NzMyQzE0Ljc0OCAxMy4yNzU3IDE0LjQ1MTcgMTMuMjc5NSAxNC40NTE3IDEzLjI3OTVDMTQuNDUxNyAxMy4yNzk1IDEzLjAwOCAxMi4xMzA4IDEyLjg3ODEgMTIuMTI0NUMxMi43MzQzIDEyLjExODMgMTEuNTU2OSAxMy4yMTIgMTEuNDU2OSAxMy4yNTgyWiIgZmlsbD0iI0ZGNTEwRiIvPgo8cGF0aCBkPSJNMTIuMzk0NSA5LjYwMkMxMi4zMDA4IDkuNjE3IDEyLjI3OTUgOS42ODE5OSAxMi4yNzk1IDkuNzY4MjRDMTIuMjc5NSA5Ljg1NDQ4IDEyLjI4MDggMTAuNzExOSAxMi4yODA4IDEwLjc2OTRDMTIuMjgwOCAxMC44MjY5IDEyLjI4MDggMTAuODg0NCAxMi4zNjA4IDEwLjg5MTlDMTIuNDQwOCAxMC44OTk0IDEzLjI2MzIgMTAuODk5NCAxMy4zMDU3IDEwLjg5OTRDMTMuMzcwNyAxMC44OTk0IDEzLjQwNyAxMC44MTk0IDEzLjQwNyAxMC43MTk0QzEzLjQwNyAxMC42MTgyIDEzLjQxMiA5Ljc3NDQ5IDEzLjQwNyA5LjcxNjk5QzEzLjM5OTUgOS42MjMyNSAxMy4zMTMyIDkuNjAyIDEzLjI0MDcgOS41OTQ1QzEzLjE2ODIgOS41ODU3NSAxMi40NTk1IDkuNTkyIDEyLjM5NDUgOS42MDJaIiBmaWxsPSIjRjE5MDFCIi8+CjxwYXRoIGQ9Ik0xLjc3MzY4IDQuNzM2MTZMNi4wMjk3MSAyLjcwNzUyQzYuMDI5NzEgMi43MDc1MiA3LjQ1ODM4IDMuMDI1IDEwLjI0MiAzLjcwODcyQzEzLjQ4MzEgNC41MDQ5MiAxNC4xMzY4IDQuNzk0OTEgMTQuMTM2OCA0Ljc5NDkxTDEzLjE2MDYgNS4wNjM2NUwxMi4zNjY5IDQuNzcxMTZMNi4wNDIyMSAzLjI1NzQ5TDIuNjQ3MzkgNC44MzI0MUwxLjkwMjQyIDQuOTQyNEwxLjc3MzY4IDQuNzM2MTZaIiBmaWxsPSIjODU4NTg1Ii8+CjxwYXRoIGQ9Ik0xNi41NDkyIDYuNjQ0NzdDMTYuNDgxNyA2LjU3MzUyIDE0LjU1OTMgNC42ODg2MiAxNC40NjY4IDQuNjg4NjJDMTQuMzc0MyA0LjY4ODYyIDcuNTEyMTMgNC42ODg2MiA3LjUxMjEzIDQuNjg4NjJDNy41MTIxMyA0LjY4ODYyIDYuMzk4NDQgMy4wMTg3MSA2LjI1NTk1IDIuODEyNDdDNi4wOTU5NiAyLjU3OTk4IDUuOTUyMjIgMi41Njc0OSA1LjgwNzIyIDIuODA2MjJDNS43NDg0OCAyLjkwMzcyIDQuNjc3MjggNC42NzYxMiA0LjY3NzI4IDQuNjc2MTJDNC42NzcyOCA0LjY3NjEyIDEuODUyNDMgNC43MTIzNyAxLjY4OTk0IDQuNjk5ODdDMS41Mjc0NSA0LjY4ODYyIDEuNDgxMiA0Ljc2OTg3IDEuNDgxMiA0LjgzODYyQzEuNDgxMiA0LjkwODYxIDEuNDgxMiA3LjExODUgMS41MTYyIDcuMTQxQzEuNTUxMiA3LjE2MzQ5IDQuMzg2MDUgNy4xNTIyNCA0LjM4NjA1IDcuMTUyMjRMNC4zNjIzIDE1LjU3NjhDNC4zNjIzIDE1LjU3NjggNC4xMDk4MSAxNS41ODE4IDMuOTM2MDcgMTUuNTgxOEMzLjc2MjMzIDE1LjU4MTggMy42NjYwOSAxNS41ODQzIDMuNjY3MzQgMTUuNzE2OEMzLjY2NzM0IDE1Ljc4NjggMy42NjQ4NCAxNi4wODY4IDMuNjY3MzQgMTYuMTkxOEMzLjY3MjM0IDE2LjM5MTggMy42NTEwOSAxNi40OTQzIDMuNzgzNTggMTYuNDkzQzMuODk5ODIgMTYuNDkxOCA3Ljg0NTg3IDE2LjQ5MyA4LjA2NTg1IDE2LjQ5M0M4LjI4NTg0IDE2LjQ5MyA4LjI4NTg0IDE2LjM1NDMgOC4yODU4NCAxNi4yODQzQzguMjg1ODQgMTYuMjE0MyA4LjI4NTg0IDE1Ljg0NDMgOC4yODU4NCAxNS43MjkzQzguMjg1ODQgMTUuNjE0MyA4LjE4MjEgMTUuNTY2OCA4LjEwMDg1IDE1LjU2NjhDOC4wMTk2MSAxNS41NjY4IDcuNjAzMzggMTUuNTY2OCA3LjYwMzM4IDE1LjU2NjhWOS4xNjcxNEM3LjYwMzM4IDkuMTY3MTQgOC4wMzIxIDkuMTkwODkgOC40OTQ1OCA5LjEyMDg5QzguOTU3MDYgOS4wNTA4OSA5LjEwNzA1IDguNTM4NDIgOS4xODgyOSA4LjE2ODQ0QzkuMjY5NTQgNy43OTg0NiA5LjI0NzA0IDcuMTIzNSA5LjI0NzA0IDcuMTIzNUM5LjI0NzA0IDcuMTIzNSAxNi40MDkyIDcuMTQyMjQgMTYuNTYwNCA3LjA4NDc1QzE2LjcxMDQgNy4wMjYgMTYuNzU1NCA2Ljg2NjAxIDE2LjU0OTIgNi42NDQ3N1pNMi4xNjYxNiA1LjI3NjA5SDMuMDAzNjJMMi4xNjYxNiA2LjIxMTA0QzIuMTY2MTYgNS45MjQ4MSAyLjE2NjE2IDUuMjY4NTkgMi4xNjYxNiA1LjI3NjA5Wk0yLjYyMzY0IDYuMzQyMjlMMy4yNjg2MSA1LjYxMTA4TDQuMDQxMDcgNi4zNDIyOUgyLjYyMzY0Wk00LjQyMzU1IDYuMTM2MDVMMy41NTQ4NCA1LjI3NjA5SDQuNDIzNTVWNi4xMzYwNVpNNi44Nzg0MiAxMy4wOTQ0TDUuOTk4NDYgMTMuOTg0NEw1LjExMzUxIDEzLjA5NDRINi44Nzg0MlpNNS4zMzIyNSAxMi42NzgyTDYuMDI0NzEgMTEuOTYzMkw2LjczNTkyIDEyLjY2OTVMNS4zMzIyNSAxMi42NzgyWk01Ljk3MjIxIDguNTkzNDJMNC45NzQ3NyA3LjY1NDcySDYuODkzNDJMNS45NzIyMSA4LjU5MzQyWk03LjEwMjE1IDguMTA4NDRWOS42NTgzNkw2LjMwNTk1IDguOTA4NEw3LjEwMjE1IDguMTA4NDRaTTYuNzkwOTIgMTAuMDY0Nkg1LjE1NzI2TDUuOTY1OTYgOS4yNTA4OEw2Ljc5MDkyIDEwLjA2NDZaTTcuMTAyMTUgMTAuNzkzM1YxMi4zNjU3TDYuMzMwOTUgMTEuNjE0NUw3LjEwMjE1IDEwLjc5MzNaTTYuNzQ4NDIgMTAuNDczM0w1Ljk4NzIxIDExLjMwODNMNS4xNzYwMSAxMC40ODgzTDYuNzQ4NDIgMTAuNDczM1pNNy4xNjU5IDYuNjA0NzdINS40MDIyNEw3LjE2NzE1IDUuNTgyMzNWNi42MDQ3N0g3LjE2NTlaTTYuMDEzNDYgMy40NjM2OUM2LjAxMzQ2IDMuNDYzNjkgNi44MDg0MiA0LjYxNzM4IDYuNzg4NDIgNC42NTYxM0M2Ljc3ODQyIDQuNjc2MTIgNi4zNzM0NCA0LjY2MTEzIDUuOTg4NDYgNC42NTg2M0M1LjYyNTk4IDQuNjU3MzggNS4yODEgNC42Njk4OCA1LjI3NzI1IDQuNjQ2MTNDNS4yNjcyNSA0LjU5ODYzIDYuMDEzNDYgMy40NjM2OSA2LjAxMzQ2IDMuNDYzNjlaTTQuOTY2MDIgNS4yNjczNEg2Ljc1ODQyTDQuOTU4NTIgNi4zNTM1NEw0Ljk2NjAyIDUuMjY3MzRaTTQuODg5NzcgOC4xODg0NEw1LjY0MDk4IDguOTMwOUw0Ljg4OTc3IDkuNjk1ODZWOC4xODg0NFpNNC44ODk3NyAxMC44NDJMNS42NjIyMyAxMS42MjA4TDQuODk0NzcgMTIuNDMwN0w0Ljg4OTc3IDEwLjg0MlpNNC44ODk3NyAxMy41NDU3TDUuNjY1OTggMTQuMzIxOUw0Ljg4OTc3IDE1LjEwNjhWMTMuNTQ1N1pNNS4xMTM1MSAxNS41ODU2TDYuMDAzNDYgMTQuNjU5M0w2LjkxMjE2IDE1LjU2ODFDNi41MDU5NCAxNS41NjY4IDUuNTQ5NzQgMTUuNTc5MyA1LjExMzUxIDE1LjU4NTZaTTcuMTAyMTUgMTUuMDk1Nkw2LjMyOTcgMTQuMzE4MUw3LjEwMjE1IDEzLjUxNDRWMTUuMDk1NlpNNy42MzIxMyA2LjYwNDc3TDcuNjM5NjMgNS40OTM1OEw4LjY5NzA3IDYuNjA3MjdDOC4wNTA4NSA2LjYwNDc3IDcuNjMyMTMgNi42MDQ3NyA3LjYzMjEzIDYuNjA0NzdaTTkuMjU4MjkgNi41MTQ3OEw4LjAwNTg2IDUuMjM2MUwxMC41MjcgNS4yMjg2TDkuMjU4MjkgNi41MTQ3OFpNOS44MjcwMSA2LjYwOTc3TDExLjA0ODIgNS40MTQ4NEwxMi4zMjMxIDYuNjE2MDJDMTEuNDgzMiA2LjYxMzUyIDEwLjU5ODIgNi42MTEwMiA5LjgyNzAxIDYuNjA5NzdaTTExLjQ4MDcgNS4yMjYxTDE0LjE4OTMgNS4yMTg2TDEyLjg1MTkgNi41MjcyOEwxMS40ODA3IDUuMjI2MVpNMTUuNTk4IDYuNjIzNTJDMTUuNTg1NSA2LjYyMzUyIDE0LjY3NjggNi42MjEwMiAxMy40NzgxIDYuNjE4NTJDMTMuNzcwNiA2LjMyMzU0IDE0LjM2OCA1LjcxOTgyIDE0LjYxNDMgNS40NzEwOEMxNC45NDU1IDUuODA3MzIgMTUuNTAwNSA2LjM3MjI5IDE1LjU4OCA2LjQ1OTc4QzE1LjcxNDIgNi41ODQ3NyAxNS42MzY3IDYuNjIzNTIgMTUuNTk4IDYuNjIzNTJaIiBmaWxsPSIjRjM5MTFDIi8+CjxwYXRoIGQ9Ik04LjEzOTY1IDcuMzM0NjZMOC4xNDQ2NSA4LjIzODM3TDguOTM5NjEgOC44MzA4NEM4LjkzOTYxIDguODMwODQgOS4xMjk2IDguNjA5NiA5LjIxNTg0IDguMTE3MTJDOS4yODgzNCA3LjcwNTg5IDkuMjQ4MzQgNy4zMjM0MSA5LjI0ODM0IDcuMzIzNDFDOS4yNDgzNCA3LjMyMzQxIDguMTUyMTUgNy4zMTk2NiA4LjEzOTY1IDcuMzM0NjZaIiBmaWxsPSIjQTdEMEQ2Ii8+CjxwYXRoIGQ9Ik03LjU5MjA4IDcuMDkzNTZDNy41OTIwOCA3LjA5MzU2IDcuNjAyMDggNy45Mjk3NiA3LjU4MjA4IDcuOTM5NzZDNy41NjIwOCA3Ljk0OTc2IDQuMzY2IDcuOTMyMjYgNC4zMDYwMSA3LjkxMjI2QzQuMjQ2MDEgNy44OTIyNiA0LjI0NzI2IDcuMTAzNTYgNC4yNzYwMSA3LjA5MzU2QzQuMzA0NzYgNy4wODM1NiA3LjU5MjA4IDcuMDkzNTYgNy41OTIwOCA3LjA5MzU2WiIgZmlsbD0iI0QxNTExNiIvPgo8L3N2Zz4K', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTguODc5NTQgMS45NDYzM0M1LjU4OTcxIDEuODcxMzQgMS45NjExNSA0LjU5OTk0IDIuMDMxMTUgOS4wMTk3MUMyLjA5MjQgMTIuOTA1OCA1LjAxMjI0IDE1Ljg0NjggOC43OTU3OSAxNS45MDgxQzEyLjU3OTMgMTUuOTY5MyAxNS45MzE3IDEzLjQ2ODIgMTUuOTUxNyA4Ljk5OTcxQzE1Ljk3MjkgNC40OTYyIDEyLjQ5ODEgMi4wMjg4MyA4Ljg3OTU0IDEuOTQ2MzNaIiBmaWxsPSIjRkYyQTIzIi8+CjxwYXRoIGQ9Ik04Ljg4OTUgMy41MjM2MUM2LjI3OTY0IDMuNjEyMzYgMy42NDYwMyA1LjU1OTc1IDMuNTg0NzggOC45OTMzMkMzLjUyMzU0IDEyLjQyNjkgNi41MjQ2MyAxNC4zMzkzIDguODg5NSAxNC4zMzkzQzExLjg5MTggMTQuMzM5MyAxNC40MDA1IDExLjk3NDQgMTQuMzM4IDguODI4MzNDMTQuMjc2NyA1LjY4MzUgMTEuOTExOCAzLjQyMTEyIDguODg5NSAzLjUyMzYxWiIgZmlsbD0iI0ZGRkRGRSIvPgo8cGF0aCBkPSJNOC45NTA3OCA0Ljg2MTEyQzYuNTg1OSA0LjgwNzM4IDQuODM4NSA2Ljg5NzI3IDQuODE3MjUgOC44OTA5MUM0Ljc5NzI1IDEwLjkyNzEgNi4wNzA5MyAxMy4wMDMyIDguOTA5NTMgMTMuMTA1N0MxMS43NDgxIDEzLjIwODIgMTMuMDgzMSAxMC45NDcxIDEzLjEyNDMgOS4wMzQ2NUMxMy4xNjY4IDcuMTIyMjUgMTEuNjQ0NCA0LjkyMjM3IDguOTUwNzggNC44NjExMloiIGZpbGw9IiNGRjJBMjMiLz4KPHBhdGggZD0iTTYuMjQ3MTggOC45NjU4QzYuMjI3MTggMTAuNzU0NSA3LjUwMDg2IDExLjc4MzIgOS4wNDMyOCAxMS43ODMyQzEwLjQ0NDUgMTEuNzgzMiAxMS44MTk0IDEwLjg5OTUgMTEuODE5NCA5LjAyODNDMTEuODE5NCA3LjM0MjE0IDEwLjU0NDUgNi4zMzQ2OSA5LjEyNTc4IDYuMjczNDVDNy43MDcxIDYuMjEwOTUgNi4yNjcxOCA3LjIzOTY0IDYuMjQ3MTggOC45NjU4WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTcuNTEyMDggOC45NTIwOEM3LjQzNTgzIDEwLjAxODMgOC4zMDMyOCAxMC41MjgzIDkuMDg0NDkgMTAuNTI4M0M5LjgyNDQ1IDEwLjUyODMgMTAuNTU0NCAxMC4wNDIgMTAuNTc1NyA5LjA1NDU4QzEwLjU5NDQgOC4xMjcxMyA5Ljg2Njk1IDcuNTYwOTEgOS4wODQ0OSA3LjUzMzQxQzguMjc3MDQgNy41MDU5MSA3LjU3MzMyIDguMDg1ODggNy41MTIwOCA4Ljk1MjA4WiIgZmlsbD0iI0ZCMkIyMiIvPgo8cGF0aCBkPSJNOS4zNDIwMSA5LjAzNTg5TDguOTIzMjggOS4yNjgzOEw5LjcyMTk5IDEwLjQwNDZDOS43MjE5OSAxMC40MDQ2IDEwLjMzOTUgMTAuNDUyMSAxMC40ODE5IDEwLjI5MDhDMTAuNjI0NCAxMC4xMjk2IDEwLjQ2MzIgOS42NTQ2MSAxMC40NjMyIDkuNjU0NjFMOS4zNDIwMSA5LjAzNTg5WiIgZmlsbD0iI0NDMTkzNSIvPgo8cGF0aCBkPSJNMTEuOTI2OCA3LjgwMDk1QzExLjgxNDMgNy41Njg0NiAxMS42OTkzIDcuMzgyMjIgMTEuMzk0NCA3LjQzOTcyQzExLjA5MDYgNy40OTcyMiAxMC41Mjk0IDcuODE5NyAxMC40MjQ0IDcuOTcyMTlDMTAuMzI4MiA4LjExMjE5IDEwLjIzNDQgOC4yODU5MyAxMC4yMzQ0IDguMjg1OTNDMTAuMjM0NCA4LjI4NTkzIDkuMTU5NDcgOC44NzM0IDguOTg5NDggOC45Njk2NEM4LjcyMzI1IDkuMTIyMTMgOC45MDY5OSA5LjQzMjEyIDkuMDk0NDggOS4zOTcxMkM5LjM1MDcxIDkuMzQ5NjIgMTAuNDk1NyA4Ljg4MzQgMTAuNDk1NyA4Ljg4MzRDMTAuNDk1NyA4Ljg4MzQgMTAuNzM4MSA4Ljk0MDg5IDEwLjg4MDYgOC45NDA4OUMxMS4wMjMxIDguOTQwODkgMTEuODM1NiA4LjUzOTY2IDExLjk0NTYgOC40NjU5MkMxMi4xMjY4IDguMzQyMTcgMTIuMDQwNiA4LjAzODQ0IDExLjkyNjggNy44MDA5NVoiIGZpbGw9IiNBQ0IxQjUiLz4KPHBhdGggZD0iTTEwLjQ1OTQgOS42NTIxQzEwLjQ1OTQgOS42NTIxIDEwLjM5OTQgOS44NjQ1OSAxMC4xODA3IDEwLjA4NDZDOS45NTMyMiAxMC4zMTIxIDkuNzE5NDggMTAuNDAyMSA5LjcxOTQ4IDEwLjQwMjFMMTAuNDQwNyAxMS40NTk1QzEwLjQ0MDcgMTEuNDU5NSAxMC45NzY5IDExLjQ5ODMgMTEuMzY2OSAxMS4xMTgzQzExLjc1NjkgMTAuNzM4MyAxMS41ODY5IDEwLjI0MDggMTEuNTg2OSAxMC4yNDA4TDEwLjQ1OTQgOS42NTIxWiIgZmlsbD0iI0M4QzhDOCIvPgo8cGF0aCBkPSJNMTEuMTc0NCAxMi41NzA2TDEwLjQzOTUgMTEuNDU4MkMxMC40Mzk1IDExLjQ1ODIgMTAuNTYxOSAxMS4zOTMyIDEwLjcyMzIgMTEuMjgzMkMxMS4wMTk0IDExLjA4MDcgMTEuMTg0NCAxMC45MTk1IDExLjM5NTcgMTAuNTg5NUMxMS41MTA2IDEwLjQxMiAxMS41ODQ0IDEwLjIzOTUgMTEuNTg0NCAxMC4yMzk1TDEyLjcyMDYgMTAuODAwN0MxMi43MjA2IDEwLjgwMDcgMTIuNjk1NiAxMS42NzgyIDEyLjMyNDQgMTIuMTA1N0MxMS45NTMxIDEyLjUzMzEgMTEuMTc0NCAxMi41NzA2IDExLjE3NDQgMTIuNTcwNloiIGZpbGw9IiNDQzE5MzUiLz4KPHBhdGggZD0iTTExLjE3NDQgMTIuNTcwN0wxMS44MDU2IDEzLjUxNjlDMTEuODA1NiAxMy41MTY5IDEyLjYyOTMgMTMuNTk4MiAxMy4zMDQzIDEyLjc4OTVDMTMuOTc5MiAxMS45ODA3IDEzLjc5OTIgMTEuMzE1OCAxMy43OTkyIDExLjMxNThMMTIuNzIwNSAxMC43OTk2QzEyLjcyMDUgMTAuNzk5NiAxMi41MTA2IDExLjMwNDUgMTIuMDkzMSAxMS43ODk1QzExLjY3NTYgMTIuMjc0NSAxMS4xNzQ0IDEyLjU3MDcgMTEuMTc0NCAxMi41NzA3WiIgZmlsbD0iI0M4QzhDOCIvPgo8cGF0aCBkPSJNMTIuNzQwNSAxNC45MTU3TDExLjgwNjggMTMuNTE4MkMxMS44MDY4IDEzLjUxODIgMTIuNTEwNSAxMy4xMDcgMTMuMDEwNSAxMi41MTU4QzEzLjUyOTIgMTEuOTAyMSAxMy44MDA1IDExLjMxODQgMTMuODAwNSAxMS4zMTg0TDE1LjMxMTYgMTIuMDY5NkMxNS4zMTE2IDEyLjA2OTYgMTQuOTcxNyAxMi45NDgzIDE0LjIxNzkgMTMuNzQyQzEzLjQ4MDUgMTQuNTE1NyAxMi43NDA1IDE0LjkxNTcgMTIuNzQwNSAxNC45MTU3WiIgZmlsbD0iI0NDMTkzNSIvPgo8cGF0aCBkPSJNMTEuMjAwNiA4LjE3MDkzQzExLjM0MTkgOC40MzQ2NiAxMS41NzU2IDguNDM3MTYgMTEuODIzMSA4LjMzMjE3QzEyLjA3MDYgOC4yMjcxNyAxMy4yMzkzIDcuNDAwOTcgMTMuMjM5MyA3LjQwMDk3QzEzLjIzOTMgNy40MDA5NyAxNC44MjY3IDguMTk5NjggMTUuODUyOSA3Ljc5OTdDMTYuODc5MSA3LjQwMDk3IDE2Ljg1MDMgNi4zMTcyOCAxNi44NTAzIDYuMzE3MjhMMTYuMjQyOSA1LjkzMjNDMTYuMjQyOSA1LjkzMjMgMTYuNjg1NCA1LjcwMjMxIDE2LjgyNzggNS42MTczMUMxNi44OTc4IDUuNTc0ODEgMTcuMDAxNiA1LjQxNzMyIDE2Ljk0NDEgNS4yNzYwOEMxNi44OTkxIDUuMTY2MDkgMTYuNzI2NiA1LjEyODU5IDE2LjU4NDEgNS4xODYwOUMxNi40NDE2IDUuMjQzNTggMTYuMDMwNCA1LjM3MjMzIDE2LjAzMDQgNS4zNzIzM0MxNi4wMzA0IDUuMzcyMzMgMTYuMTk0MSA0LjY5MTExIDE2LjE2NTQgNC42NTM2MUMxNi4xMzY2IDQuNjE2MTIgMTUuODQxNiA0LjQ2MzYyIDE1Ljg0MTYgNC40NjM2MkMxNS44NDE2IDQuNDYzNjIgMTQuODgxNyA0LjQ2MzYyIDE0Ljc1OCA0LjUxMTEyQzE0LjYzNDIgNC41NTg2MiAxMy42ODQzIDUuNDk5ODIgMTMuNjg0MyA1LjQ5OTgyTDEzLjIyOCA2LjQzMTAyTDEzLjAzMyA2Ljg0NkMxMy4wMzMgNi44NDYgMTEuNzAxOSA3LjQ0MjIyIDExLjUwMTkgNy41MjcyMUMxMS4zMDMxIDcuNjEzNDYgMTEuMDI2OSA3Ljg0OTY5IDExLjIwMDYgOC4xNzA5M1oiIGZpbGw9IiMwMDZDQTkiLz4KPHBhdGggZD0iTTEzLjMxNjggNi43MzEwOEMxMy4zMTY4IDYuNzMxMDggMTMuODc2OCA2LjMyNjEgMTQuMjc1NSA2LjEyNjExQzE0LjY3NDIgNS45MjYxMiAxNS40MTQyIDUuNjgyMzkgMTUuNjMyOSA1LjY1NjE0QzE1Ljg1MTcgNS42Mjk4OSAxNS45NDE3IDUuNjg4NjQgMTUuOTgwNCA1Ljc5MTEzQzE2LjAxOTIgNS44OTM2MyAxNS44OTA0IDYuMDIyMzcgMTUuNzgwNCA2LjA4MTEyQzE1LjY3MTcgNi4xMzg2MSAxNS4wOTE3IDYuNDAyMzUgMTQuNDQ5MiA2Ljc0MzU4QzEzLjgwNjggNy4wODQ4MSAxMy41MDE4IDcuMzM0OCAxMy4yNDE4IDcuMzkzNTVDMTMuMTEwNiA3LjQyMzU1IDEyLjk0MTggNy4zMDYwNSAxMi45MzE4IDcuMTA5ODFDMTIuOTI0MyA2Ljk1NzMyIDEyLjk1NDMgNi44Mjk4MyAxMy4wNTQzIDYuNTM3MzRDMTMuMTc2OCA2LjE3NzM2IDEzLjQ0NTUgNS42MzYxNCAxMy42NzE4IDUuMjgzNjZDMTQuMDA2OCA0Ljc2MjQ0IDE0LjQ4OTIgNC40MDg3MSAxNS4wMjMgNC4zMTI0NkMxNS41NTY3IDQuMjE2MjIgMTUuOTk0MiA0LjM1MTIxIDE2LjEwNDIgNC40NDEyQzE2LjIxMjkgNC41MzEyIDE2LjIwNjYgNC42MjM2OSAxNi4xNjc5IDQuNjU0OTRDMTYuMTI5MiA0LjY4NjE5IDE2LjA3MTcgNC43MTExOSAxNS43ODE3IDQuNjA4NjlDMTUuNTgwNCA0LjUzNzQ1IDE0LjkzOTIgNC41Mzc0NSAxNC40ODkyIDQuODcyNDNDMTQuMDM0MyA1LjIwOTkxIDEzLjkyMyA1LjQxODY1IDEzLjcyNDMgNS43ODYxM0MxMy41MjU1IDYuMTUzNjEgMTMuMzE2OCA2LjczMTA4IDEzLjMxNjggNi43MzEwOFoiIGZpbGw9IiMxRjg3RkQiLz4KPC9zdmc+Cg==', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEuNzY4NzEgMTMuNDI2OUMxLjc2ODcxIDEyLjIyMDggNS4wMDYwNCAxMS41NjA4IDguOTk5NTggMTEuNTYwOEMxMi45OTMxIDExLjU2MDggMTYuMjMwNCAxMi4yMjA4IDE2LjIzMDQgMTMuNDI2OUMxNi4yMzA0IDE1LjEyNDQgMTIuOTkzMSAxNi40OTkzIDguOTk5NTggMTYuNDk5M0M1LjAwNjA0IDE2LjQ5OTMgMS43Njg3MSAxNS4xMjQ0IDEuNzY4NzEgMTMuNDI2OVoiIGZpbGw9IiM5NEQxRTAiLz4KPHBhdGggZD0iTTIuMzkzNjggNy4zMzQ3MlYxMy4wMjgySDIuMzk3NDNDMi40ODExNyAxNC41MDkzIDUuMzg3MjcgMTUuNjk5MyA4Ljk2MjA4IDE1LjY5OTNDMTIuNTM2OSAxNS42OTkzIDE1LjQ0MyAxNC41MDkzIDE1LjUyNjcgMTMuMDI4MkgxNS41MzA1VjcuMzM0NzJIMi4zOTM2OFoiIGZpbGw9IiNFNjNGMzMiLz4KPHBhdGggZD0iTTMuOTI4NTkgMTAuNDU0NEM1LjI4OTc3IDExLjAyMTkgNy4wNzcxOCAxMS4zMzQ0IDguOTYyMDggMTEuMzM0NEMxMC44NDcgMTEuMzM0NCAxMi42Mzk0IDExLjAzMzEgMTMuOTk1NiAxMC40NTQ0QzE1LjIxNDIgOS45MzQ0NSAxNS4zMzggOS41ODk0NyAxNS41MzA1IDkuMzkwNzNWOC42MzQ1MkMxNS4xNzQzIDkuMTA3IDE0LjU3MyA5LjUzMDcyIDEzLjc1MTggOS44NzE5NkMxMi40NjU2IDEwLjQwODIgMTAuNzY0NSAxMC43MDMyIDguOTYwODMgMTAuNzAzMkM3LjE1NzE3IDEwLjcwMzIgNS40NTYwMSAxMC40MDgyIDQuMTY5ODMgOS44NzE5NkMzLjM0OTg3IDkuNTI5NDcgMi43NDg2NiA5LjEwNyAyLjM5MTE3IDguNjM0NTJWOS41NTU3MkwzLjAyMjM5IDEwLjAwMDdDMy4yOTQ4OCAxMC4xNjQ0IDMuNTk2MTEgMTAuMzE1NyAzLjkyODU5IDEwLjQ1NDRaIiBmaWxsPSIjQkRCREJEIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNi4zOTM0NSAxMC4zNDM4QzYuNTM0NzIgMTAuMzQzOCA2LjY0OTI0IDEwLjQ1ODMgNi42NDkyNCAxMC41OTk1VjE1LjY2MDVDNi42NDkyNCAxNS44MDE4IDYuNTM0NzIgMTUuOTE2MyA2LjM5MzQ1IDE1LjkxNjNDNi4yNTIxOCAxNS45MTYzIDYuMTM3NjYgMTUuODAxOCA2LjEzNzY2IDE1LjY2MDVWMTAuNTk5NUM2LjEzNzY2IDEwLjQ1ODMgNi4yNTIxOCAxMC4zNDM4IDYuMzkzNDUgMTAuMzQzOFoiIGZpbGw9IiM5NEQxRTAiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMS41MzA3IDEwLjM0MzhDMTEuNjcyIDEwLjM0MzggMTEuNzg2NSAxMC40NTgzIDExLjc4NjUgMTAuNTk5NVYxNS42NjA1QzExLjc4NjUgMTUuODAxOCAxMS42NzIgMTUuOTE2MyAxMS41MzA3IDE1LjkxNjNDMTEuMzg5NCAxNS45MTYzIDExLjI3NDkgMTUuODAxOCAxMS4yNzQ5IDE1LjY2MDVWMTAuNTk5NUMxMS4yNzQ5IDEwLjQ1ODMgMTEuMzg5NCAxMC4zNDM4IDExLjUzMDcgMTAuMzQzOFoiIGZpbGw9IiM5NEQxRTAiLz4KPHBhdGggZD0iTTYuMjUwOTggMTQuMjU1NkM2LjEzNDc0IDE0LjIxMzEgNi4wMzA5OSAxNC4xMjQ0IDUuOTcxIDE0LjAxNjlDNS44NjQ3NSAxMy44MjU2IDUuODczNSAxMy41ODA2IDUuODY5NzUgMTMuMzM1NkM1Ljg2MjI1IDEyLjgwMTkgNi4wMDk3NCAxMi42Njk0IDYuMTgzNDggMTIuNjY5NEg2LjYwMjIxQzYuOTE1OTUgMTIuNzk4MiA2LjkxNTk1IDEzLjA1NjkgNi45MTU5NSAxMy4yMzA3QzYuOTE1OTUgMTMuNDExOSA2LjkxNTk1IDEzLjM0NjkgNi45MTU5NSAxMy41MjgxQzYuOTE1OTUgMTMuNzkwNiA2LjkyOTY5IDE0LjA1MzEgNi42Nzg0NiAxNC4yMTk0QzYuNjU0NzEgMTQuMjM1NiA2LjQ1OTcyIDE0LjMzMTggNi4yNTA5OCAxNC4yNTU2WiIgZmlsbD0iIzk0RDFFMCIvPgo8cGF0aCBkPSJNMTEuNjY5NCAxNC4yNTU2QzExLjc4MTkgMTQuMjA0NCAxMS44ODA3IDE0LjExODEgMTEuOTQ5NCAxNC4wMTY5QzEyLjA5ODEgMTMuNzk4MSAxMi4wNDY5IDEzLjU4MDYgMTIuMDUwNyAxMy4zMzU2QzEyLjA1ODIgMTIuODMwNyAxMS45MTA3IDEyLjY2OTQgMTEuNzM2OSAxMi42Njk0SDExLjMxODJDMTEuMDA0NSAxMi43OTgyIDExLjAwNDUgMTMuMDU2OSAxMS4wMDQ1IDEzLjIzMDdDMTEuMDA0NSAxMy40MTE5IDExLjAwNDUgMTMuMzQ2OSAxMS4wMDQ1IDEzLjUyODFDMTEuMDA0NSAxMy43OTA2IDExLjAwOTUgMTQuMDI2OSAxMS4yNDE5IDE0LjIxOTRDMTEuMzQ5NCAxNC4zMDgxIDExLjU1NTcgMTQuMzA4MSAxMS42Njk0IDE0LjI1NTZaIiBmaWxsPSIjOTREMUUwIi8+CjxwYXRoIGQ9Ik0xNi4wNDA1IDExLjM4NTZDMTYuMDI4IDEwLjc5NjkgMTUuOTIwNSAxMC42OTU3IDE1LjY0MDUgMTAuNzE5NEMxNS42NDA1IDkuNjM5NDcgMTUuNjQwNSA4LjY2NTc3IDE1LjY0MDUgOC42NjU3N0wxNS4yMjggOC45MjA3NkwxNS4yNTggMTMuODI1NUMxNS4yNTggMTMuODI1NSAxNS42MzkyIDEzLjk0MyAxNS42MzkyIDEzLjUzMThDMTUuNjM5MiAxMy40MTA1IDE1LjYzOTIgMTIuOTM4IDE1LjYzOTIgMTIuMzMxOEMxNi4wNDY3IDEyLjE4OTMgMTYuMDY4IDExLjc1NTYgMTYuMDQwNSAxMS4zODU2WiIgZmlsbD0iIzk0RDFFMCIvPgo8cGF0aCBkPSJNMi42ODYxNSA4Ljg1NzA0TDIuMjczNjcgOC42MDIwNUMyLjI3MzY3IDguNjAyMDUgMi4yNzM2NyA5LjYxNDUgMi4yNzM2NyAxMC43MTk0QzEuOTkzNjkgMTAuNjk1NyAxLjg4NjE5IDEwLjc5NjkgMS44NzM2OSAxMS4zODU3QzEuODczNjkgMTEuNjMwNiAxLjgzMjQ0IDExLjg0ODEgMS45NjExOSAxMi4wNjY5QzIuMDM0OTMgMTIuMTg5NCAyLjE0MTE4IDEyLjI5MzEgMi4yNzM2NyAxMi4zMzE5QzIuMjczNjcgMTIuOTE0MyAyLjI3MzY3IDEzLjM2NTUgMi4yNzM2NyAxMy40ODNDMi4yNzM2NyAxMy44OTQzIDIuNjU0OSAxMy43NzY4IDIuNjU0OSAxMy43NzY4TDIuNjg2MTUgOC44NTcwNFoiIGZpbGw9IiM5NEQxRTAiLz4KPHBhdGggZD0iTTE1Ljg5NDIgNy41NjIxMUMxNS42OTA1IDUuMTU1OTkgMTEuMzQ5NCA0LjQ3NzI3IDguOTYyMDcgNC40ODQ3N0M2LjU5NTk1IDQuNDY0NzcgMi4wNTExOSA1LjE5NTk4IDIuMDMyNDQgNy42NjcxQzAuNzI4NzU5IDkuOTk0NDggNC44NjEwNCAxMS41NTE5IDguOTk5NTcgMTEuNTYxOUMxMy4xNTE5IDExLjU3MTkgMTcuNDQwNCA5LjkxNjk5IDE1Ljg5NDIgNy41NjIxMVoiIGZpbGw9IiM5NEQxRTAiLz4KPHBhdGggZD0iTTguOTYyMDggMTAuMzIzMUMxMi41ODk3IDEwLjMyMzEgMTUuNTMwNSA5LjA5NzU0IDE1LjUzMDUgNy41ODU3NEMxNS41MzA1IDYuMDczOTQgMTIuNTg5NyA0Ljg0ODM5IDguOTYyMDggNC44NDgzOUM1LjMzNDQ1IDQuODQ4MzkgMi4zOTM2OCA2LjA3Mzk0IDIuMzkzNjggNy41ODU3NEMyLjM5MzY4IDkuMDk3NTQgNS4zMzQ0NSAxMC4zMjMxIDguOTYyMDggMTAuMzIzMVoiIGZpbGw9IiNGRkNDODAiLz4KPHBhdGggZD0iTTguOTYyMDUgMTAuMzY1N0MxMi4zMTA4IDEwLjM2NTcgMTUuMDI1NSA5LjIzNDczIDE1LjAyNTUgNy44Mzk1OUMxNS4wMjU1IDYuNDQ0NDYgMTIuMzEwOCA1LjMxMzQ4IDguOTYyMDUgNS4zMTM0OEM1LjYxMzMxIDUuMzEzNDggMi44OTg2MiA2LjQ0NDQ2IDIuODk4NjIgNy44Mzk1OUMyLjg5ODYyIDkuMjM0NzMgNS42MTMzMSAxMC4zNjU3IDguOTYyMDUgMTAuMzY1N1oiIGZpbGw9IiNGRUY2RTAiLz4KPHBhdGggZD0iTTcuNzM4MzkgOS4zODQ1MkwzLjM2OTg3IDYuNDgyMTdMNC4wNTk4NCA2LjE0ODQ0TDcuOTQyMTMgOS4wOTgyOEw3LjczODM5IDkuMzg0NTJaIiBmaWxsPSIjRkZDQzgwIi8+CjxwYXRoIGQ9Ik0xMC4yOTMyIDkuMzg0NTJMMTQuNjYwNSA2LjQ4MjE3TDEzLjk3MTggNi4xNDg0NEwxMC4wODgzIDkuMDk4MjhMMTAuMjkzMiA5LjM4NDUyWiIgZmlsbD0iI0ZGQ0M4MCIvPgo8cGF0aCBkPSJNOC4yMDQ2MiA4LjgzMDg0QzguMDI4MzggOC41OTIxIDcuNzQyMTQgOC41MzgzNiA3LjcwNTg5IDguNDcyMTFMNy41NjM0IDguMjE3MTJDNy41MTQ2NSA4LjEyOTYzIDcuNDc4NCA4LjA3NDYzIDcuNDM4NDEgOC4wMjIxM0wyLjQ4MzY3IDEuNjQyNDdDMi4zNjYxNyAxLjQ4OTk4IDIuMTQ5OTQgMS40NTQ5OCAxLjk4ODY5IDEuNTYyNDhDMS44MDk5NSAxLjY4MzcyIDEuNzczNzEgMS45MzI0NiAxLjkxMTIgMi4wOTg3TDYuOTEzNDMgOC4zOTk2MUM2Ljk3OTY4IDguNDgyMTEgNy4wNTQ2OCA4LjU1NzExIDcuMTM4NDIgOC42MjA4NUw3LjM0ODQxIDguNzgyMDlDNy40MDQ2NiA4LjgyNDU5IDcuMzk0NjYgOS4wOTU4MyA3LjU5MzQgOS4yNzgzMkM3Ljc1ODM5IDkuNDI5NTYgOC4wMTU4OCA5LjQ4NDU2IDguMTc5NjIgOS4zMTIwN0M4LjMwNTg2IDkuMTgwODIgOC4zMDgzNiA4Ljk3MDgzIDguMjA0NjIgOC44MzA4NFoiIGZpbGw9IiNCQTc5M0UiLz4KPHBhdGggZD0iTTkuODUzMjYgOC44MzA4NEMxMC4wMjk1IDguNTkyMSAxMC4zMTU3IDguNTM4MzYgMTAuMzUyIDguNDcyMTFMMTAuNDk0NSA4LjIxNzEyQzEwLjU0MzIgOC4xMjk2MyAxMC41Nzk1IDguMDc0NjMgMTAuNjE5NSA4LjAyMjEzTDE1LjU3NDIgMS42NDI0N0MxNS42OTMgMS40ODk5OCAxNS45MDkyIDEuNDU0OTggMTYuMDY5MiAxLjU2MjQ4QzE2LjI0NzkgMS42ODM3MiAxNi4yODQyIDEuOTMyNDYgMTYuMTQ2NyAyLjA5ODdMMTEuMTQ0NCA4LjQwMDg2QzExLjA3ODIgOC40ODMzNiAxMS4wMDMyIDguNTU4MzYgMTAuOTE5NSA4LjYyMjFMMTAuNzA5NSA4Ljc4MzM0QzEwLjY1MzIgOC44MjU4NCAxMC42NjMyIDkuMDk3MDggMTAuNDY0NSA5LjI3OTU3QzEwLjI5OTUgOS40MzA4MSAxMC4wNDIgOS40ODU4MSA5Ljg3ODI2IDkuMzEzMzJDOS43NTIwMiA5LjE4MDgyIDkuNzQ5NTIgOC45NzA4MyA5Ljg1MzI2IDguODMwODRaIiBmaWxsPSIjQkE3OTNFIi8+CjxwYXRoIGQ9Ik0zLjM4MTA4IDEwLjAyOTVDMi42NTQ4NyA5LjU3ODI0IDIuMzIxMTQgOS4xMjIwMiAyLjM3OTg5IDguOTcwNzhDMi40MTczOCA4Ljg3NDUzIDIuNTU2MTMgOC44NzQ1MyAyLjYyMjM3IDguOTUzMjhDMy4yNzg1OSA5LjcyNTc0IDQuNjIzNTIgMTAuMjY5NSA1LjU5NDcyIDEwLjQ0OTRDNS42OTU5NiAxMC40NjgyIDUuNzczNDYgMTAuNjU2OSA1LjUwMDk3IDEwLjY4MzJDNS4yNjQ3MyAxMC43MDQ0IDQuMzM5NzggMTAuNjI1NyAzLjM4MTA4IDEwLjAyOTVaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTEuNDQ5NCAxNC4wMDE5QzExLjM2NjkgMTQuMDM1NiAxMS4yNzQ0IDEzLjkzNTYgMTEuMjM1NyAxMy44MDQ0QzExLjE5NjkgMTMuNjczMSAxMS4xNzQ0IDEzLjIxNTYgMTEuMTk2OSAxMy4wODE5QzExLjIxODIgMTIuOTQ5NCAxMS4yNDU3IDEyLjg2ODIgMTEuMzQ5NCAxMi44NjgyQzExLjQ1MzIgMTIuODY4MiAxMS41MzY5IDEyLjk1MTkgMTEuNTM2OSAxMy4wNTU3QzExLjUzNjkgMTMuMDU1NyAxMS41MjQ0IDEzLjU1NTYgMTEuNTMwNyAxMy42NzY5QzExLjUzNjkgMTMuODAwNiAxMS41MzE5IDEzLjk2ODEgMTEuNDQ5NCAxNC4wMDE5WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuMzA1OTYgMTQuMDAxOUM2LjIyMzQ3IDE0LjAzNTYgNi4xMzA5NyAxMy45MzU2IDYuMDkyMjIgMTMuODA0NEM2LjA1MzQ4IDEzLjY3MzEgNi4wMzA5OCAxMy4yMTU2IDYuMDUzNDggMTMuMDgxOUM2LjA3NTk3IDEyLjk0ODIgNi4xMDIyMiAxMi44NjgyIDYuMjA1OTcgMTIuODY4MkM2LjMwOTcxIDEyLjg2ODIgNi4zOTM0NiAxMi45NTE5IDYuMzkzNDYgMTMuMDU1N0M2LjM5MzQ2IDEzLjA1NTcgNi4zODA5NiAxMy41NTU2IDYuMzg3MjEgMTMuNjc2OUM2LjM5MzQ2IDEzLjgwMDYgNi4zODg0NiAxMy45NjgxIDYuMzA1OTYgMTQuMDAxOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEuNTM4NjYgMi40MzM5MkMxLjMyNDkyIDIuODA3NjUgMS42NTk5IDMuNjAyNjEgMy4yOTczMSAzLjkyNzU5QzUuMzM1OTYgNC4zMzI1NyA3LjUzMjA5IDMuNTEyNjEgOS4xODgyNSAzLjgwMTM1QzEwLjEzMDcgMy45NjYzNCAxMC43MjgyIDQuMTgwMDggMTEuMDcwNyA0LjMzNTA3QzExLjMzNjkgNC40NTYzMSAxMS4zMzY5IDQuNDY2MzEgMTEuMzM2OSA0LjQ2NjMxTDExLjc1OTQgMy4xMjYzOEMxMS43NTk0IDMuMTI2MzggMTAuNTgzMiAyLjQ4ODkyIDkuMDA1NzYgMi40MTg5MkM3LjQyODM1IDIuMzQ4OTMgNi4yNTU5MSAyLjg2NTE1IDQuNjkyMjQgMi44MjM5QzIuMzM0ODcgMi43NjE0IDEuODczNjQgMS44NDc3IDEuNTM4NjYgMi40MzM5MloiIGZpbGw9IiM5NjY3MzgiLz4KPHBhdGggZD0iTTEzLjc3OTIgNi43MTYxM0wxNS4wNzY3IDYuMTg2MTZDMTUuMDc2NyA2LjE4NjE2IDE1Ljc0OTEgNy4xMjQ4NiAxNS45ODY2IDguOTY3MjZDMTYuMjcwNCAxMS4xNjg0IDE1LjU4MTYgMTIuNjI0NiAxNS44MDU0IDEzLjg2N0MxNi4wMjkxIDE1LjEwOTQgMTYuNTE3OCAxNS43Nzk0IDE2LjQ3NTMgMTYuMDczMUMxNi40MzI4IDE2LjM2NjkgMTYuMjI0MSAxNi42ODY5IDE1Ljk0NTQgMTYuNTM0NEMxNS42NjY2IDE2LjM4MDYgMTQuNzk2NyAxNS4yNjMyIDE0Ljc1OTIgMTMuNzE0NUMxNC43MTY3IDExLjk5NzEgMTUuMTkyOSAxMC41NjQ3IDE0LjgwMTcgOS4wMzg1MUMxNC4zNjY3IDcuMzQ3MzUgMTMuNzc5MiA2LjcxNjEzIDEzLjc3OTIgNi43MTYxM1oiIGZpbGw9IiM5NjY3MzgiLz4KPHBhdGggZD0iTTExLjY1OTQgMy4wNzYyOUwxMS4wOTgxIDQuMzQ4NzNDMTEuMDk4MSA0LjM0ODczIDExLjg0OTQgNC42Nzk5NiAxMi42NzgxIDUuNDM0OTJDMTMuNDAxOCA2LjA5NDg4IDEzLjkyMDUgNi45MDEwOSAxMy45MjA1IDYuOTAxMDlMMTUuMDc5MiA2LjE4ODYzQzE1LjA3OTIgNi4xODg2MyAxNC40NzggNS4xMjM2OSAxMy41OTkzIDQuMzU5OThDMTIuNTUzMSAzLjQ1MDAyIDExLjY1OTQgMy4wNzYyOSAxMS42NTk0IDMuMDc2MjlaIiBmaWxsPSIjRDI5RjZDIi8+CjxwYXRoIGQ9Ik0xMy44MzY4IDMuNjI3NTZMNS4yMTQ3MiAxMi40NTQ2TDUuMjUyMjIgMTIuODY0Nkw1LjYyNDcgMTIuOTIwOEwxNC4zNDA1IDQuMTEyNTRMMTMuODM2OCAzLjYyNzU2WiIgZmlsbD0iI0YzQzk3NiIvPgo8cGF0aCBkPSJNMTQuMjE2NyAzLjIwMjZMMTMuNzY4IDMuNzAzODJMMTQuMjYwNSA0LjE5NjNMMTQuNzgwNSAzLjcyMTMyQzE0Ljc4MDUgMy43MjEzMiAxNS42MDc5IDMuODgwMDYgMTUuNjk2NyAzLjgyNzU3QzE1LjgxNzkgMy43NTUwNyAxNi41NDQxIDEuNTA4OTQgMTYuNTA2NiAxLjQ1MDE5QzE2LjQ0NTQgMS4zNTM5NSAxNS42ODY3IDEuNjU3NjggMTUuMjA0MiAxLjgyODkyQzE0LjYzMTcgMi4wMzE0MSAxNC4xNDggMi4yMjY0IDE0LjEwNDIgMi4yODY0QzE0LjAwNTUgMi40MTg4OSAxNC4yMTY3IDMuMjAyNiAxNC4yMTY3IDMuMjAyNloiIGZpbGw9IiM4NTg1ODUiLz4KPHBhdGggZD0iTTE1LjQxOTIgMTYuMjUzMUMxNS4zOTc5IDE2LjI1MzEgMTUuMzc1NCAxNi4yNTA2IDE1LjM1NDIgMTYuMjQ0NEw0LjkzODQ4IDEzLjQyMkM0Ljg1MzQ5IDEzLjM5ODMgNC43ODU5OSAxMy4zMzIxIDQuNzYzNDkgMTMuMjQ3MUwyLjEwODYzIDMuNjE3NTZDMi4wOTYxMyAzLjU3MTMyIDIuMDk2MTMgMy41MjM4MiAyLjExMTEzIDMuNDc3NTdMMi40MzM2MSAyLjQxMTM4TDIuOTExMDkgMi41NjYzN0wyLjY2MzYgMy43MzAwNkw1LjE4NzIyIDEyLjkxNDZMMTUuMzE5MiAxNS43NDU3TDE2LjA3NDEgMTQuODc0NUwxNi4yNjQxIDE1LjM2NTdMMTUuNjEyOSAxNi4xNjQ0QzE1LjU2MjkgMTYuMjIxOSAxNS40OTI5IDE2LjI1MzEgMTUuNDE5MiAxNi4yNTMxWiIgZmlsbD0iI0I4Q0VENCIvPgo8cGF0aCBkPSJNNS4yMjU5OCAxMi41MjQ1TDUuNjI0NyAxMi45MjA4QzUuNjI0NyAxMi45MjA4IDUuMjAzNDggMTMuNDMyIDUuMTM1OTggMTMuNDM4MkM1LjA5NTk4IDEzLjQ0MiA0Ljc0OTc1IDEzLjE1ODMgNC43NzcyNSAxMy4wOTU4QzQuODA0NzUgMTMuMDM0NSA1LjIyNTk4IDEyLjUyNDUgNS4yMjU5OCAxMi41MjQ1WiIgZmlsbD0iIzVENjI2NSIvPgo8cGF0aCBkPSJNNy40NjIwOCAxMC4yMjFDNy40NTgzMyAxMC4yMzEgNi43OTcxMiAxMC45MjcyIDYuMTg3MTUgMTEuNTcwOUM1LjcxNDY4IDEyLjA2OTcgNS4yNjcyIDEyLjU1MzQgNS4yMzM0NSAxMi41NTM0QzUuMTU3MjEgMTIuNTUzNCA0LjQ2ODQ5IDExLjkwMzQgNC40NTcyNCAxMS43NTIyQzQuNDQ1OTkgMTEuNjAwOSA1LjQ5OTY5IDEwLjY5MjIgNS45OTk2NiAxMC4zMzg1QzcuMDMyMTEgOS42MDcyOCA3LjQ2MjA4IDEwLjIyMSA3LjQ2MjA4IDEwLjIyMVoiIGZpbGw9IiNEQjBEMkEiLz4KPHBhdGggZD0iTTcuODU0NTMgMTAuNjI0NkM3LjgyODI5IDEwLjY0ODQgNy4wNzA4MyAxMS40MjA4IDYuNDQyMTEgMTIuMDYyMUM2LjAxMDg4IDEyLjUwMiA1LjYyODQgMTIuODc4MyA1LjYyMzQgMTIuOTE0NUM1LjYxNzE1IDEyLjk2NTggNi4yMDA4NyAxMy42NDA3IDYuMjYyMTIgMTMuNjQwN0M2LjM3NDYxIDEzLjY0MDcgNy40NzgzIDEyLjQ2MzMgNy44MjA3OSAxMS44Njk2QzguMzI1NzYgMTAuOTk0NiA3Ljg1NDUzIDEwLjYyNDYgNy44NTQ1MyAxMC42MjQ2WiIgZmlsbD0iI0RCMEQyQSIvPgo8L3N2Zz4K', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcuMDIyMTcgOS41NTU4MUwxLjgzNzQ0IDkuNzQzM0MxLjgzNzQ0IDkuNzQzMyAxLjUwODcxIDkuNDE0NTYgMS41NTYyMSA5LjI5NzA3QzEuNjAzNyA5LjE3OTU4IDUuODk3MjMgNS44MjQ3NSA2LjA2MDk3IDUuODQ4NUM2LjIyNDcxIDUuODcyMjUgNi42MDA5NCA2LjI3MDk4IDYuOTA1OTIgNi4xMjk3NEM3LjIxMDkxIDUuOTg4NDkgNi45MDU5MiA1LjE2NzI5IDYuOTk5NjcgNS4wNTEwNEM3LjA5MzQxIDQuOTMzNTUgMTAuNDQ4MiAzLjEyNzM5IDEwLjY1OTUgMy4wMzM2NUMxMC44NzA3IDIuOTM5OSAxMS4wNTgyIDIuNzI4NjcgMTEuNDgwNyAyLjgyMjQxQzExLjkwMzIgMi45MTYxNiAxMi45NTk0IDMuMTAzNjUgMTQuMTA4IDMuODc4NkMxNS4yNTY3IDQuNjUzNTYgMTYuNDU0MiA1LjU0NDc3IDE2LjM2MDQgNi4wMzcyNEMxNi4yNjY3IDYuNTI5NzIgNy4wMjIxNyA5LjU1NTgxIDcuMDIyMTcgOS41NTU4MVoiIGZpbGw9IiNGRkUyNjUiLz4KPHBhdGggZD0iTTMuNDMyMzMgMTUuNDQ1NUM0LjU4MjI3IDE1LjMwNDIgMTUuMzA0MiAxMy4wMDU2IDE1Ljc1MDQgMTIuOTM1NkMxNi4xOTY3IDEyLjg2NTYgMTYuNDc3OSAxMi40NDMxIDE2LjQ3NzkgMTEuOTczMkMxNi40Nzc5IDExLjUwMzIgMTYuMzYwNCA2LjAzNzIzIDE2LjM2MDQgNi4wMzcyM0MxNi4zNjA0IDYuMDM3MjMgMTUuODQ0MiA2LjIwMDk3IDE1LjIzNDIgNi4zNDIyMkMxNC42MjQyIDYuNDgyMjEgOS42MjU3NiA3LjcwMjE0IDcuNjU1ODYgOC4xNDgzN0M1LjY4NTk2IDguNTk0NiAyLjU0MTEzIDkuNDE1OCAyLjE4ODY1IDkuNDE1OEMxLjgzNjE3IDkuNDE1OCAxLjU1NDkzIDkuMjk4MzEgMS41NTQ5MyA5LjI5ODMxQzEuNTU0OTMgOS4yOTgzMSAxLjY0ODY4IDE0LjE3ODEgMS42NzI0MyAxNC41MzA1QzEuNjk2MTcgMTQuODgzIDEuODU5OTIgMTUuMzA0MiAyLjI1ODY0IDE1LjQyMTdDMi42NTczNyAxNS41MzkyIDIuOTE3MzYgMTUuNTA4IDMuNDMyMzMgMTUuNDQ1NVoiIGZpbGw9IiNGRUI1MDIiLz4KPHBhdGggZD0iTTEwLjM3MDggNC4xOTQ4M0MxMC4zNzA4IDQuNDk0ODIgMTAuNjc1OCA0LjY5MzU2IDExLjE4MDggNC42Nzg1NkMxMS43MzIgNC42NjIzMSAxMS44ODQ1IDQuMzM0ODMgMTEuODY3IDQuMTI0ODRDMTEuODQ5NSAzLjkxMzYgMTEuNTk1OCAzLjcyODYxIDExLjA5MzMgMy43MzczNkMxMC42MjcxIDMuNzQ2MTEgMTAuMzcwOCAzLjkzMTEgMTAuMzcwOCA0LjE5NDgzWiIgZmlsbD0iI0ZGOEIwRCIvPgo8cGF0aCBkPSJNNC43Mzk4OCA3LjkzNDU4QzMuOTI3NDIgOC4yMDgzMiA0LjAxODY3IDguOTE5NTMgNC4yNTYxNiA5LjA3ODI3QzQuNDkzNjQgOS4yMzcwMSA0LjgwOTg4IDkuMjI4MjYgNS4xNzk4NiA5LjA5NTc3QzUuNTQ5ODQgOC45NjMyOCA1Ljc4NDgzIDguODEyMDMgNS44MjIzMiA4LjU2NDU1QzUuODU3MzIgOC4zMjk1NiA1LjU0OTg0IDcuNjYwODQgNC43Mzk4OCA3LjkzNDU4WiIgZmlsbD0iI0VENkIzMSIvPgo8cGF0aCBkPSJNMTIuMTIyIDUuOTAyMjVDMTEuNTc3MSA1LjkyNzI1IDEwLjk3NDYgNi4zMTg0OCAxMC43NDk2IDYuNjk0NzFDMTAuNTEyMSA3LjA5MDk0IDEwLjQ5NDYgNy41NTcxNiAxMS4wMzk2IDcuODQ3MTVDMTEuNTg0NiA4LjEzNzEzIDEzLjUxMiA3LjM5ODQyIDEzLjQxNTcgNi43MjA5NkMxMy4zMTk1IDYuMDQyMjQgMTIuNjk0NSA1Ljg3NiAxMi4xMjIgNS45MDIyNVoiIGZpbGw9IiNFRDZCMzEiLz4KPHBhdGggZD0iTTEwLjIzMDcgMTEuMzgzMkMxMC4yMzA3IDExLjM4MzIgMTEuMDA0NSAxMC44MTA3IDExLjAwNDUgMTAuNDA3QzExLjAwNDUgMTAuMDAyIDEwLjY3MDcgOS4wNDU4MyA5LjI1MzI5IDguOTczMzRDNy45MjQ2MSA4LjkwNDU5IDcuMTA1OTEgMTAuNzE1NyA3Ljk3NzExIDExLjY0ODJDOC44NDgzMiAxMi41ODA2IDkuMzc1NzkgMTIuMTU4MiA5LjcwMjAyIDEyLjA3MDdDMTAuMDI4MyAxMS45ODE5IDEwLjIzMDcgMTEuMzgzMiAxMC4yMzA3IDExLjM4MzJaIiBmaWxsPSIjRUQ2QjMxIi8+CjxwYXRoIGQ9Ik02LjQwMzM5IDEzLjAxMDZDNi40MDMzOSAxMy4wMTA2IDcuMTcyMDkgMTIuNTI5NCA2LjU4MjEzIDEyLjAwMTlDNi4xNjcxNSAxMS42MzA3IDUuNjEyMTggMTEuNzk4MiA1LjMyOTY5IDEyLjA4NjlDNC45Njg0NiAxMi40NTY5IDUuMDY1OTYgMTMuMDQ1NiA1LjM5OTY5IDEzLjMwOTNDNS43MzQ2NyAxMy41NzQzIDYuNDAzMzkgMTMuMDEwNiA2LjQwMzM5IDEzLjAxMDZaIiBmaWxsPSIjRUQ2QjMxIi8+CjxwYXRoIGQ9Ik0zLjc5ODU3IDEwLjg0NjlDMy43OTg1NyAxMC44NDY5IDMuNzExMDggMTAuNTcwNyAzLjM5NzM0IDEwLjQ1OTRDMy4wNDQ4NiAxMC4zMzQ1IDIuNTE4NjQgMTAuNzczMiAyLjc0MzYzIDExLjI0MTlDMi45NDYxMiAxMS42NjQ0IDMuMzk0ODQgMTEuNTQwNiAzLjM5NDg0IDExLjU0MDZMMy43OTg1NyAxMC44NDY5WiIgZmlsbD0iI0VENkIzMSIvPgo8cGF0aCBkPSJNMTUuMDUxOCAxMS4yNTA3QzE1LjEyODEgMTEuMTY4MiAxNS42MjY4IDEwLjgwODMgMTUuNjI2OCAxMC41MzQ1QzE1LjYyNjggMTAuMjYyIDE0Ljg3NTYgOS40MTk1OCAxMy45MTY5IDEwLjA1MzNDMTIuOTU4MiAxMC42ODcgMTMuNDU5NCAxMS42NDU3IDEzLjcyMzIgMTEuODA0NUMxMy45ODY5IDExLjk2MzIgMTQuNzE4MSAxMS42MTIgMTUuMDUxOCAxMS4yNTA3WiIgZmlsbD0iI0VENkIzMSIvPgo8cGF0aCBkPSJNNC44NTczNSA4LjM2NTg1QzQuMzIzNjMgOC41NTA4NCA0LjEyMjM5IDguOTI4MzIgNC4yNTYxMyA5LjA3ODMxQzQuMzk3MzcgOS4yMzcwNSA0LjcwNDg2IDkuMzQyMDUgNS4yNDEwOCA5LjE0ODMxQzUuNjU2MDYgOC45OTgzMSA1Ljg2NjA1IDguNzQ3MDggNS44MzEwNSA4LjUyMzM0QzUuODA2MDUgOC4zNjA4NSA1LjM0MTA4IDguMTk4MzYgNC44NTczNSA4LjM2NTg1WiIgZmlsbD0iI0ZGOEIwRCIvPgo8cGF0aCBkPSJNMy4wNzczNiAxMC44MjgyQzIuODQzNjIgMTEuMTIwNyAzLjAzMzYxIDExLjQ1MzIgMy4yNTM2IDExLjUzMTlDMy40NzM1OSAxMS42MTA3IDMuODYxMDYgMTEuMzczMiAzLjgzNDgyIDEwLjk3ODJDMy44MDczMiAxMC41ODIgMy4yODg2IDEwLjU2NDUgMy4wNzczNiAxMC44MjgyWiIgZmlsbD0iI0ZGOEIwRCIvPgo8cGF0aCBkPSJNNS43OTU5OSAxMi40MjE5QzUuMjkzNTIgMTIuODI5NCA1LjI3OTc3IDEzLjIyODEgNS40NzM1MSAxMy4zNjMxQzUuNjA4NSAxMy40NTgxIDYuMTA1OTcgMTMuNjU1NiA2LjU2MDk1IDEzLjMxOTRDNi44OTg0MyAxMy4wNzA2IDYuOTA0NjggMTIuNTQ4MiA2Ljc5ODQ0IDEyLjI5ODJDNi43MDk2OSAxMi4wODU3IDYuMjE4NDcgMTIuMDc4MiA1Ljc5NTk5IDEyLjQyMTlaIiBmaWxsPSIjRkY4QjBEIi8+CjxwYXRoIGQ9Ik04Ljg0NTgxIDEwLjIwMDdDOC4xNzMzNCAxMC44NTQ0IDguMDEzMzUgMTEuOTE1NiA4LjY3MzMyIDEyLjE2NTZDOS4yNTQ1NCAxMi4zODU2IDEwLjA4OTUgMTIuMjUzMSAxMC41ODIgMTEuNjQ2OUMxMS4wOTk0IDExLjAwOTQgMTEuMTU0NCAxMC4zMDU3IDEwLjgyMiA5Ljg5MDcyQzEwLjQyNDUgOS4zOTIgOS4zNjU3OCA5LjY5Njk4IDguODQ1ODEgMTAuMjAwN1oiIGZpbGw9IiNGRjhCMEQiLz4KPHBhdGggZD0iTTExLjkwMTkgNi41NTM0N0MxMS4wMjIgNi44NTIyMSAxMC41MDMzIDcuNDg1OTIgMTAuOTUyIDcuODAzNDFDMTEuNDAwNyA4LjExOTY0IDExLjg2NjkgOC4wNzMzOSAxMi4zNzY5IDcuODk5NjVDMTIuOTU1NiA3LjcwMzQxIDEzLjM5ODEgNy4yNDg0NCAxMy40MTU2IDYuODE3MjFDMTMuNDI5NCA2LjQ2MzQ4IDEyLjY3NjkgNi4yODk3NCAxMS45MDE5IDYuNTUzNDdaIiBmaWxsPSIjRkY4QjBEIi8+CjxwYXRoIGQ9Ik0xNC4yNTE4IDEwLjU5OTVDMTMuNjUzMSAxMS4wODcgMTMuNDc4MSAxMS41NzU3IDEzLjcyNDMgMTEuODA0NEMxMy45NzA2IDEyLjAzMzIgMTQuNTYwNiAxMi4xMzgyIDE1LjEwNTUgMTEuNzYwN0MxNS42NzE3IDExLjM2ODIgMTUuNzEzIDEwLjkyMzIgMTUuNjUwNSAxMC41NjQ1QzE1LjU4MDUgMTAuMTYwOCAxNC44NjY4IDEwLjA5ODMgMTQuMjUxOCAxMC41OTk1WiIgZmlsbD0iI0ZGOEIwRCIvPgo8L3N2Zz4K', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTguNzk0NiAxNi4wNDU1QzguNzk0NiAxNi4wNDU1IDEzLjM1MTkgMTUuNTczIDE0LjIxMTggMTQuODIwNUMxNC42NDE4IDE0LjQ0NDMgMTUuMTMwNSAxMy42NDA2IDE1LjQxNTUgMTMuMTY1NkMxNS43MDA1IDEyLjY4OTQgMTUuODg4IDEyLjM0ODIgMTYuMDYwNSAxMS44NzU3QzE2LjQwNDIgMTAuOTI5NSAxNi41NzY3IDguMDkyMTQgMTYuMDYwNSA2LjY3MzQ3QzE1LjU0NDIgNS4yNTQ3OSAxNC44NzggMy45ODYxMSAxMy45NTMxIDMuMjMzNjVDMTMuMDQ0NCAyLjQ5MTE5IDExLjQzODIgMS40ODk5OSA4Ljk4NzA5IDEuNDg5OTlDNy44OTM0IDEuNDg5OTkgNy4wNjQ2OSAxLjcxOTk4IDYuMzY0NzMgMS45NjI0N0M1LjMxNzI4IDIuMzI2MiA0LjcyMjMxIDIuNzIzNjggNC4zMjIzNCAzLjA1ODY2QzMuODQyMzYgMy40NjIzOSAyLjg5MjQxIDQuMjk2MDkgMi4yMzc0NSA1Ljc4OTc2QzEuOTgxMjEgNi4zNzIyMyAxLjY5ODcyIDcuMTg4NDQgMS42MTM3MyA3LjkxODRDMS4zMTI0OSAxMC41MTk1IDEuNzg0OTcgMTIuNzEzMiAzLjI4OTg5IDE0LjI2MDZDNC43OTQ4MSAxNS44MDggNS4zNTM1MyAxNS41OTMgNS4zNTM1MyAxNS41OTNMOC43OTQ2IDE2LjA0NTVaIiBmaWxsPSIjRjQ3QTAwIi8+CjxwYXRoIGQ9Ik00LjkwOTczIDE0LjEwM0M0LjkwOTczIDE0LjEwMyA0LjY3MzUgMTQuMTAzIDQuMzkzNTEgMTMuOTc0M0M0LjExMzUzIDEzLjg0NTYgMy4wODIzMyAxMi40MDQ0IDIuODg4NTkgMTEuODQ1N0MyLjY5NDg1IDExLjI4NjkgMi4yNTM2MyA5LjMxOTU1IDIuNDU4NjEgOC4zODQ2QzIuNTc2MTEgNy44NTA4OCAyLjcxNjEgNy40Mzg0IDIuNzE2MSA3LjQzODRDMi43MTYxIDcuNDM4NCAzLjIzNjA3IDYuMDM1OTcgMy4zMDEwNyA1LjU4NDc1QzMuMzY2MDcgNS4xMzM1MiAzLjk0MjI5IDQuNjIyMyA0LjIyMTAyIDQuMjk5ODFDNC40OTk3NiAzLjk3NzMzIDQuNjkzNSAzLjY1NDg1IDUuMjA5NzIgMy4zNTM2MUM1LjcyNTk0IDMuMDUyMzggNi4yNjM0MSAzLjA1MjM4IDYuNjA3MTUgMi44ODExNEM2Ljk1MDg4IDIuNzA4NjUgNy40NjA4NSAyLjM5NzQxIDcuOTYyMDcgMi4yNTc0MkM4LjkxMzI3IDEuOTkxMTkgOS40ODgyNCAyLjI3ODY3IDEwLjAwNDUgMi4yNTc0MkMxMC41MjA3IDIuMjM2MTcgMTEuMDA5NCAyLjM5ODY2IDExLjY0MzEgMi43Mzg2NUMxMi4wOTQ0IDIuOTgxMTMgMTIuNDQzMSAzLjI1OTg3IDEyLjg1MDYgMy41ODIzNUMxMy4yNTggMy45MDQ4MyAxMy44MjQzIDQuMDk4NTcgMTQuMjM5MiA0LjYyMTA1QzE0LjYzNjcgNS4xMjEwMiAxNC43MTE3IDUuNjk1OTkgMTQuOTQ5MiA2LjM0MDk2QzE1LjE4NTQgNi45ODU5MiAxNS40NDU0IDcuMzI1OSAxNS40ODkyIDcuNzU1ODhDMTUuNTMxNyA4LjE4NTg2IDE1LjQ5NDIgOC41MjMzNCAxNS41MTU0IDkuMDE3MDdDMTUuNTM2NyA5LjUxMDc5IDE1LjY0NjcgOS44ODA3NyAxNS42MjE3IDEwLjE2ODNDMTUuNTY5MiAxMC43ODgyIDE1LjMzMTcgMTAuOTI0NSAxNS4yNjc5IDExLjM1NDRDMTUuMjAyOSAxMS43ODQ0IDE0LjkyOCAxMi4yODY5IDE0Ljc4OTIgMTIuNTYwNkMxNC40MTQyIDEzLjMwMTggMTMuNzI1NSAxMy43MDMxIDEzLjQzNTUgMTMuODk1NkMxMi45MzE4IDE0LjIzNDMgNC45MDk3MyAxNC4xMDMgNC45MDk3MyAxNC4xMDNaIiBmaWxsPSIjRkZBNzI2Ii8+CjxwYXRoIGQ9Ik0zLjEzOTc2IDguNzQ0NTJDMi42OTYwMyA4LjY4NTc3IDIuMjkxMDUgOS4xMzk0OSAyLjMxOTggOS42NzMyMkMyLjM0OTggMTAuMjA2OSAyLjcwNDc4IDEwLjM4NDQgMy4wODk3NiAxMC4yODU3QzMuNTc3MjQgMTAuMTYwNyAzLjc1MDk4IDkuNzEzMjEgMy43NDA5OCA5LjM4Njk4QzMuNzMwOTggOS4wNjA3NSAzLjU4MjI0IDguODAzMjYgMy4xMzk3NiA4Ljc0NDUyWiIgZmlsbD0iI0YyN0IwMCIvPgo8cGF0aCBkPSJNNS45NTg0OCA1LjQxODQ1QzUuNTY5NzUgNS4xMDM0NyA1LjcxMjI1IDQuNjc0NzQgNS45MjIyNCA0LjQwMjI1QzYuMTMyMjIgNC4xMjk3NyA2LjcxMDk0IDQuMDAzNTMgNi45NTIxOCA0LjQyMzVDNy4xOTM0MiA0Ljg0MzQ4IDcuMDEzNDMgNS4xMzU5NyA2LjgyMjE5IDUuMzM1OTZDNi42MzQ3IDUuNTMzNDQgNi4yMTcyMiA1LjYyODQ0IDUuOTU4NDggNS40MTg0NVoiIGZpbGw9IiNGMjdCMDAiLz4KPHBhdGggZD0iTTcuNjAzNDEgNC4yNzg1NEM3Ljc3MDkgNC42MTQ3NyA4LjI0NDYyIDQuNjczNTIgOC41NjA4NiA0LjQ5NjAzQzguODc3MDkgNC4zMTg1NCA4Ljk4NTgzIDMuOTcyMzEgOC43OTgzNCAzLjY1NzMyQzguNjEzMzUgMy4zNDYwOSA4LjEzNzEzIDMuMjAzNiA3Ljc5MDkgMy40Nzk4M0M3LjQ0NTkyIDMuNzU0ODIgNy41MDQ2NiA0LjA4MTA1IDcuNjAzNDEgNC4yNzg1NFoiIGZpbGw9IiNGMjdCMDAiLz4KPHBhdGggZD0iTTkuNTQxODYgNS41MDcyMkM5LjY5NTYxIDUuODYyMiAxMC4xMzMxIDUuOTE5NyAxMC40NzE4IDUuNzUwOTZDMTAuODEwNSA1LjU4MjIyIDExLjA0OTMgNS4xMzA5OSAxMC44NDY4IDQuNzgyMjZDMTAuNjQzMSA0LjQzNDc4IDEwLjE4NjggNC40MDM1MyA5Ljg4MzEgNC42MTM1MkM5LjU3OTM2IDQuODIzNTEgOS4zNzA2MiA1LjExMjI0IDkuNTQxODYgNS41MDcyMloiIGZpbGw9IiNGMjdCMDAiLz4KPHBhdGggZD0iTTExLjM5NyA0LjU5MzUyQzExLjY4MDcgNC44MjEwMSAxMi4xNzU3IDQuNjg5NzcgMTIuMzU2OSA0LjM1NDc4QzEyLjQ5NTYgNC4wOTg1NSAxMi41NzE5IDMuODAzNTYgMTIuMjg1NyAzLjUxNzMzQzExLjk5OTQgMy4yMzEwOSAxMS41MzE5IDMuMzU4NTkgMTEuMzE3IDMuNjYxMDdDMTEuMTQ4MiAzLjkwMTA2IDExLjEwMDcgNC4zNTcyOCAxMS4zOTcgNC41OTM1MloiIGZpbGw9IiNGMjdCMDAiLz4KPHBhdGggZD0iTTQuNDc3MjIgNi4yNzM0QzQuMjY0NzMgNi4yODg0IDQuMjk1OTggNi43MjQ2MyA0LjI4NTk4IDcuMzQ3MUM0LjI3NTk4IDcuOTQyMDcgNC4xNTg0OSA5LjE5MDc1IDQuMjI0NzQgMTAuNTMwN0M0LjI1MjIzIDExLjA5NDQgNC4xOTM0OSAxMi42ODQzIDQuMjQ3MjMgMTMuNDgzQzQuMzAyMjMgMTQuMjgzIDQuMjY5NzMgMTUuMTc2NyA0LjI2OTczIDE1LjE3NjdDNC4yNjk3MyAxNS4xNzY3IDUuNTUwOTIgMTYuNTA0MSA4Ljk5NDQ4IDE2LjU1NDFDMTIuNjIxOCAxNi42MDY2IDE0LjEwMTcgMTQuODcxNyAxNC4xMDE3IDE0Ljg3MTdDMTQuMTAxNyAxNC44NzE3IDE0LjA2MTcgMTIuNjgzMSAxNC4wNjU1IDExLjgyMTlDMTQuMDcwNSAxMC41NDgyIDEzLjk5MTcgOC4xMjU4MSAxMy45OTE3IDcuNzM0NThDMTMuOTkxNyA3LjM0MzM1IDE0LjAyNDIgNi40MjA5IDEzLjg4MyA2LjI5NTlDMTMuNzQxNyA2LjE3MDkxIDEyLjczOCA2LjIyNzE2IDExLjgyMzEgNi4yNDk2NkMxMS4wMDQ0IDYuMjY5NjUgOC4xNDgyOCA2LjI3NzE1IDcuMDk4MzMgNi4yNzcxNUM2LjA0ODM5IDYuMjc3MTUgNC45MTU5NSA2LjI0MjE2IDQuNDc3MjIgNi4yNzM0WiIgZmlsbD0iIzNDM0MzQyIvPgo8cGF0aCBkPSJNNC45Njk3NCA3LjEyMzQzQzQuODM1OTkgNy4yMDQ2NyA0LjgxMzQ5IDguMjgyMTIgNC44NDQ3NCA5LjI4NTgxQzQuODc1OTkgMTAuMjg5NSA0Ljg3NTk5IDExLjI0NDUgNS4yMDQ3MiAxMS4zMDdDNS41MzM0NiAxMS4zNjk1IDUuODYyMTkgMTAuNjMzMiA2LjIwNzE3IDEwLjM1MDhDNi41NTIxNSAxMC4wNjgzIDYuOTU5NjMgOS44NDk1MyA3LjIyNTg3IDkuNDU4M0M3LjQ5MjEgOS4wNjcwNyA3Ljc4OTU5IDguNDM5NjEgNy45MzA4MyA4LjEyNzEyQzguMDcyMDcgNy44MTMzOSA4LjUxMDggNy4zOTA5MSA4LjQ2MzMgNy4xMzk2OEM4LjQxNTggNi44ODg0NCA2Ljg4MDg4IDYuOTgzNDMgNi40MTA5MSA2Ljk4MzQzQzUuOTQwOTMgNi45ODM0MyA1LjI1MjIyIDYuOTUyMTkgNC45Njk3NCA3LjEyMzQzWiIgZmlsbD0iIzg1ODU4NSIvPgo8cGF0aCBkPSJNOS4zNDA4MyA2Ljk2NzE3QzkuMTQyMDkgNy4xMDg0MSA4LjkzMzM1IDcuNjU1ODggOC43OTIxMSA3LjkwNzEyQzguNjUwODcgOC4xNTgzNiA4LjI1OTY0IDguNzA1ODMgOC41NTcxMiA4Ljg5NDU3QzguODU0NjEgOS4wODIwNiA5LjQ0OTU3IDguNjQzMzMgOS42MzgzMSA4LjUxODM0QzkuODI1OCA4LjM5MzM1IDEwLjA3NyA4LjMzMDg1IDEwLjE3MDggOC4yNTIxQzEwLjI2NDUgOC4xNzMzNiAxMC43MzQ1IDcuNDg0NjQgMTAuODQ0NSA3LjM3NDY1QzEwLjk1NDUgNy4yNjQ2NiAxMS4xODk1IDcuMTM5NjYgMTEuMTg5NSA3LjAxNDY3QzExLjE4OTUgNi44ODk2NyAxMC42MDk1IDYuODg5NjggMTAuMDkzMyA2LjkwNDY3QzkuNTc3MDcgNi45MTk2NyA5LjQ1MDgyIDYuODg4NDMgOS4zNDA4MyA2Ljk2NzE3WiIgZmlsbD0iIzg1ODU4NSIvPgo8cGF0aCBkPSJNNy41Mzk0NyAxMC4xNDdDNy4zOTQ0OCAxMC4zNDQ1IDcuMTQ4MjQgMTAuNzczMyA3LjQyOTQ4IDEwLjk3N0M3LjcxMTk2IDExLjE4MDcgOC4wMjQ0NSAxMC45NjA3IDguMjQ0NDQgMTAuNzI1OEM4LjQ2NDQyIDEwLjQ5MDggOC41NzMxNyAxMC4xNzcgOC4zMDY5MyA5Ljg5NThDOC4wNDA3IDkuNjE0NTYgNy43MTA3MSA5LjkxMjA1IDcuNTM5NDcgMTAuMTQ3WiIgZmlsbD0iIzg1ODU4NSIvPgo8cGF0aCBkPSJNNi41OTk3IDExLjI3NTdDNi40NDM0NiAxMS4yNjMyIDYuMTEzNDcgMTEuNzYyIDUuODE1OTkgMTEuODg3QzUuNTE4NTEgMTIuMDEyIDUuMDYzNTMgMTEuOTM0NSA0Ljk4NjAzIDEyLjIxNTdDNC45MDcyOSAxMi40OTgyIDQuNzM0OCAxMy42ODgxIDQuOTg2MDMgMTMuODI5NEM1LjIzNzI3IDEzLjk3MDYgNS45MjU5OCAxMy43MzU2IDYuMDgyMjMgMTMuNjQxOUM2LjIzODQ3IDEzLjU0ODEgNi4yMjM0NyAxMy4zMjgxIDYuNTM1OTUgMTMuMTI0NEM2Ljg0OTY5IDEyLjkyMDcgNi44OTU5MyAxMi4zODgyIDYuOTEyMTggMTIuMDEyQzYuOTI4NDMgMTEuNjM1NyA2LjgwMjE5IDExLjI5MDcgNi41OTk3IDExLjI3NTdaIiBmaWxsPSIjODU4NTg1Ii8+Cjwvc3ZnPgo=', + '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==', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuNjgyMjggOC40ODIwNkw0LjM1NDggNy43NzA4NUwzLjU1MzU5IDQuNjU2MDFMMy4zNDk4NSA0LjE3MTA0TDMuNjQzNTkgMy40NDg1OEMzLjY0MzU5IDMuNDQ4NTggNC4wNTk4MiAzLjUwNzMyIDQuMzc3MyAzLjg2NjA2QzQuNzYzNTMgNC4zMDM1MyA1LjM5MzUgNS42MTIyMSA1LjM5MzUgNS42MTIyMUw1LjI2MSA0LjU3OTc3TDUuOTc5NzEgMi43MTYxMkM1Ljk3OTcxIDIuNzE2MTIgNi4yOTQ3IDIuNjgyMzcgNi4zNzg0NCAyLjc2ODYxQzYuNTI3MTkgMi45MjExMSA2Ljc4MDkyIDMuOTkxMDUgNi44NzA5MiA0LjMyOTc4QzYuOTYwOTEgNC42Njg1MSA3LjA4NTkxIDUuMTMwOTkgNy4yNDM0IDUuMzMzNDhDNy40MDA4OSA1LjUzNTk3IDcuNTkzMzggNS41ODIyMiA3LjU5MzM4IDUuNTgyMjJMOC4xNTcxIDMuNDA0ODNMOC4yOTIwOSAxLjQyOTkzQzguMjkyMDkgMS40Mjk5MyA4LjQ4OTU4IDEuNDI4NjggOC43NjU4MiAxLjQ4NjE4QzguOTc0NTYgMS41Mjk5MyA5LjExNDU1IDEuNjAxMTcgOS4xODIwNSAxLjY4NDkyQzkuMjQ5NTQgMS43Njg2NyA5LjI1MjA0IDMuMDk5ODUgOS4yNTIwNCAzLjA5OTg1QzkuMjUyMDQgMy4wOTk4NSAxMC40NDMyIDIuMDg0OSAxMC40ODIgMi4wOTYxNUMxMC41NjQ1IDIuMTE4NjUgMTAuODIwNyAyLjE3NDg5IDEwLjk1NTcgMi4zMjIzOUMxMS4wOTA3IDIuNDY4NjMgMTAuOTc4MiAzLjAxMTEgMTAuODc3IDMuMzcyMzNDMTAuNzc1NyAzLjczMzU2IDEwLjQyNTcgNC45MDcyNSAxMC40MjU3IDQuOTA3MjVMMTAuNzQyIDQuNzg3MjZMMTAuODEyIDQuODU0NzVMMTEuNDAwNyAzLjk1MTA1QzExLjQwMDcgMy45NTEwNSAxMS42MTMyIDMuOTQ5OCAxMS43ODU3IDQuMDM0OEMxMS45NzMxIDQuMTI3MjkgMTIuMDg1NiA0LjM1NDc4IDEyLjA4NTYgNC4zNTQ3OEwxMy41OTgxIDIuMTY2MTVDMTMuNTk4MSAyLjE2NjE1IDEzLjcxMzEgMi4yNjg2NCAxMy44MTQzIDIuMzY5ODhDMTMuOTE1NSAyLjQ3MTEzIDEzLjk4NjggMi41NDIzOCAxMy45OTE4IDIuNjQ0ODdDMTMuOTk2OCAyLjc0NjExIDEzLjYzMTggMy40MTk4MyAxMy40NjMxIDMuNzU4NTZDMTMuMjk0MyA0LjA5NzI5IDEzLjAxMTggNC43NzQ3NiAxMy4wMTE4IDQuNzc0NzZMMTQuMjQxOCAzLjgzNjA2QzE0LjI0MTggMy44MzYwNiAxNC42NDkzIDMuODkzNTUgMTQuNjYxOCA0LjAyODU1QzE0LjY3MyA0LjE2MzU0IDE0LjQ1NjggNC40MTM1MyAxNC4yNDE4IDQuNzg2MDFDMTQuMDI2OCA1LjE1ODQ5IDEzLjY2NjggNS44ODA5NSAxMy41NDE4IDYuMTI4NDRDMTMuNDE2OCA2LjM3NTkyIDEzLjU4NjggNi4zODg0MiAxMy41ODY4IDYuMzg4NDJMMTUuMDYwNSA1LjQ1NzIyQzE1LjA2MDUgNS40NTcyMiAxNS4zNzggNS41MjIyMiAxNS40MDggNS42Njk3MUMxNS40NDE3IDUuODM4NDUgMTUuMDc2NyA2LjE3NDY4IDE0LjgzOTIgNi40NDQ2N0MxNC42MDE4IDYuNzE1OTEgMTQuMDk0MyA3LjM2OTYyIDEzLjgzNTUgNy44NzgzNEMxMy41NzY4IDguMzg3MDcgMTMuNDAzMSA4LjcwNDU1IDEzLjQwMzEgOC43MDQ1NUwxMS4xNTk0IDEwLjkzMDdMNS4xODk3NiAxMC41Njk1TDQuNjgyMjggOC40ODIwNloiIGZpbGw9InVybCgjcGFpbnQwX3JhZGlhbF83MThfODgpIi8+CjxwYXRoIGQ9Ik0xMi4xNDU4IDYuMDI0NjNDMTEuOTk1OCA2LjA1OTYzIDExLjUyMjEgNi44Nzk1OCAxMS4wNzk2IDguMTI3MDJDMTAuODEzNCA4Ljg3Njk4IDEwLjYzNzEgOS42OTY5MyAxMC42NDcxIDEwLjA4OTRDMTAuNjU3MSAxMC40ODE5IDExLjI0OTYgMTAuMzYxOSAxMS4yNDk2IDEwLjM2MTlMMTEuNTAwOCA5LjU0Njk0QzExLjUwMDggOS41NDY5NCAxMS43ODIxIDguNTkwNzQgMTIuMTE0NiA3Ljg3NzAzQzEyLjUwMzMgNy4wMzk1NyAxMy4wMDcgNi41MDcxIDEyLjk2OTUgNi4zNzgzNkMxMi45MTk1IDYuMjA1ODcgMTIuMzU3IDUuOTc0NjMgMTIuMTQ1OCA2LjAyNDYzWiIgZmlsbD0iI0ZCREU5MyIvPgo8cGF0aCBkPSJNMTMuNjM0MyAzLjg5MjI4QzEzLjQyNDMgNC4wMTQ3OCAxMi45NTA2IDQuODA3MjQgMTIuODA5NCA1LjEwOTcyQzEyLjY2ODEgNS40MTIyIDEyLjY2MzEgNS45ODA5MiAxMi44MTgxIDYuMDU0NjdDMTMuMDA5MyA2LjE0NDY3IDEzLjIyMTggNS41MDIyIDEzLjU0NDMgNC45Njg0OEMxMy44NjY4IDQuNDM0NzYgMTQuMjQ4IDMuODMxMDQgMTQuMjQ4IDMuODMxMDRDMTQuMjQ4IDMuODMxMDQgMTMuODc1NSAzLjc1MTA0IDEzLjYzNDMgMy44OTIyOFoiIGZpbGw9IiNGQkRFOTMiLz4KPHBhdGggZD0iTTE0LjQyOTMgNS40NjIyQzE0LjEwODEgNS41OTg0NSAxMy41MDk0IDYuMjM3MTYgMTMuMzAxOSA2LjU1ODRDMTIuOTE5NCA3LjE1MjEyIDEyLjYzODEgNy44MTU4MyAxMi41MzY5IDguMDg3MDdDMTIuNDM2OSA4LjM1ODMgMTIuMzU1NyA4LjczMDc4IDEyLjY2ODEgOC41NTk1NEMxMi45ODA2IDguMzg4MyAxMy4zODE5IDcuNTgzMzQgMTMuODk1NiA2Ljg1OTYzQzE0LjQ5MDUgNi4wMTk2OCAxNS4wNjMgNS40NTA5NiAxNS4wNjMgNS40NTA5NkMxNS4wNjMgNS40NTA5NiAxNC42OTA1IDUuMzUwOTYgMTQuNDI5MyA1LjQ2MjJaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik0xMy4wODA3IDEuODc5OTVDMTIuOTU2OSAxLjg2NDk1IDEyLjYxODIgMi43NjQ5IDEyLjQyNyAzLjE5NzM4QzEyLjIzNTcgMy42Mjk4NSAxMS45ODA3IDQuMjAyMzIgMTEuOTgwNyA0LjIwMjMyQzExLjk4MDcgNC4yMDIzMiAxMS44MzMzIDUuMzQxMDEgMTIuMDQ0NSA1LjM2MTAxQzEyLjI1NTcgNS4zODEwMSAxMi42Njk1IDMuOTkyMzMgMTIuOTc4MiAzLjM1NjEyQzEzLjI3OTQgMi43MzYxNSAxMy41OTY5IDIuMTYyNDMgMTMuNTk2OSAyLjE2MjQzQzEzLjU5NjkgMi4xNjI0MyAxMy4zMjE5IDEuOTA5OTQgMTMuMDgwNyAxLjg3OTk1WiIgZmlsbD0iI0ZCREU5MyIvPgo8cGF0aCBkPSJNMTAuODU1OCA0LjEzNDgxQzEwLjc0NDUgNC4yNTIzMSAxMC44MDcgNC44NTcyOCAxMC44MDcgNC44NTcyOEMxMC44MDcgNC44NTcyOCAxMS4yMzk1IDUuMTc5NzYgMTEuMzIwOCA1LjEwODUxQzExLjQwMiA1LjAzNzI3IDExLjQwNTggMy45NDQ4MiAxMS40MDU4IDMuOTQ0ODJDMTEuNDA1OCAzLjk0NDgyIDExLjAzNyAzLjk0MzU3IDEwLjg1NTggNC4xMzQ4MVoiIGZpbGw9IiNGQkRFOTMiLz4KPHBhdGggZD0iTTkuOTYyMTIgNC45NTg0OEM5LjgwNTg3IDUuMDg5NzMgOS45MTIxMiA1LjcwNDY5IDkuODcyMTIgNi4yNTU5MkM5LjgxMjEyIDcuMDgwODcgOS41NzA4OSA4Ljc3MDc4IDkuNDk5NjQgOS4yNjQ1MUM5LjQyODM5IDkuNzU4MjMgOS4zNTg0IDEwLjMwMDcgOS4zNTg0IDEwLjMwMDdMMTAuMTMzNCAxMC4xNzk1QzEwLjEzMzQgMTAuMTc5NSAxMC40MzQ2IDcuODc1ODMgMTAuNTU1OCA2Ljk4OTYzQzEwLjY3NzEgNi4xMDM0MiAxMC44NTcxIDQuODQ3MjQgMTAuNzM3MSA0Ljc3NTk5QzEwLjYxNTggNC43MDcyNSAxMC4xNTM0IDQuNzk3MjQgOS45NjIxMiA0Ljk1ODQ4WiIgZmlsbD0iI0ZCREU5MyIvPgo8cGF0aCBkPSJNOS44NjgyNyAyLjAxOTkxQzkuNjcwNzggMi4wNTc0MSA5LjI1MzMgMy4wOTk4NSA5LjI1MzMgMy4wOTk4NUM5LjI1MzMgMy4wOTk4NSA5LjIyODMgMy44NTIzMSA5LjIyODMgNC4yMTQ3OUM5LjIyODMgNC41NzcyNyA5LjE1ODMxIDUuMDA5NzUgOS4yNzgzIDUuMDA5NzVDOS4zOTgyOSA1LjAwOTc1IDkuNjMwNzggNC4zMTYwNCA5Ljg3MjAyIDMuNzEyMzJDMTAuMTEzMyAzLjEwODYgMTAuNDgzMiAyLjA5NjE1IDEwLjQ4MzIgMi4wOTYxNUMxMC40ODMyIDIuMDk2MTUgMTAuMTQ4MyAxLjk2NzQxIDkuODY4MjcgMi4wMTk5MVoiIGZpbGw9IiNGQkRFOTMiLz4KPHBhdGggZD0iTTcuNTk3MjcgMS42Mzg2MUM3LjQ2MTAzIDEuNzgyMzUgNy42NzcyNiAyLjE1MjMzIDcuNjk3MjYgMy4xNjcyOEM3LjcxNzI2IDQuMTgzNDcgNy41OTIyNyA1LjU4MDkgNy41OTIyNyA1LjU4MDlDNy41OTIyNyA1LjU4MDkgNy42OTk3NiA1LjU3MzQgNy43NjcyNiA1Ljc4MjE0QzcuODI0NzYgNS45NTgzOCA3LjkxODUgNy4wMjk1NyA4LjAzODQ5IDYuOTg5NThDOC4xNTk3NCA2Ljk0OTU4IDguMzA5NzMgNi4yMzQ2MiA4LjQzMDk3IDUuMDI3MThDOC41NTIyMiAzLjgxOTc0IDguNTAwOTcgMi44ODQ3OSA4LjQ2MDk3IDIuNTAyMzFDOC40MjA5NyAyLjExOTgzIDguMjkwOTggMS40MjczNyA4LjI5MDk4IDEuNDI3MzdDOC4yOTA5OCAxLjQyNzM3IDcuNzk4NTEgMS40MjczNyA3LjU5NzI3IDEuNjM4NjFaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik01Ljk3OTgxIDIuNzE2MDZDNS45Nzk4MSAyLjcxNjA2IDUuMzk4NTkgMi43NTczMSA1LjIzODYgMy4wNjk4QzUuMDc3MzUgMy4zODIyOCA1LjEyODYgMy41MzM1MiA1LjE1ODYgMy44MzQ3NkM1LjE4ODYgNC4xMzU5OSA1LjI4MzU5IDQuNzM3MjEgNS4yODM1OSA0LjczNzIxQzUuMjgzNTkgNC43MzcyMSA1LjgxNzMyIDUuNDAwOTIgNi4xMDg1NSA1LjIzOTY4QzYuMzk5NzggNS4wNzk2OSA1Ljk3OTgxIDIuNzE2MDYgNS45Nzk4MSAyLjcxNjA2WiIgZmlsbD0iI0ZCREU5MyIvPgo8cGF0aCBkPSJNMy42NDM2MyAzLjQ0ODU0QzMuNjQzNjMgMy40NDg1NCAzLjMyMTE0IDMuMzg4NTUgMy4xMjk5IDMuNDI4NTRDMi45Mzg2NiAzLjQ2ODU0IDIuOTQ4NjYgMy42NDk3OCAzLjAzOTkxIDMuNzcxMDNDMy4xMjk5IDMuODkyMjcgMy41NDYxMyA0LjY0NDczIDMuNTQ2MTMgNC42NDQ3M0MzLjU0NjEzIDQuNjQ0NzMgMy42Njk4OCA0LjY0NzIzIDMuODMxMTIgNC43MDcyM0MzLjk5MjM2IDQuNzY3MjIgNC4yNjg1OSA0LjkxNzIyIDQuMjU3MzQgNC42NzU5OEM0LjIzNzM1IDQuMjU0NzUgMy42NDM2MyAzLjQ0ODU0IDMuNjQzNjMgMy40NDg1NFoiIGZpbGw9IiNGQkRFOTMiLz4KPHBhdGggZD0iTTQuNTUyMzEgOC4xOTk1N0M0LjU1MjMxIDguMTk5NTcgMy45NDQ4NCA2Ljk3MDg4IDMuNDkyMzYgNi4yOTU5MkMzLjAzOTg5IDUuNjIwOTUgMi41NjYxNiA1LjA5ODQ4IDIuNTY2MTYgNC45ODg0OUMyLjU2NjE2IDQuODc3MjQgMi44Nzg2NSA0Ljc2NzI1IDMuMDU4NjQgNC43MTcyNUMzLjIzODYzIDQuNjY3MjYgMy41NTIzNiA0LjY1NjAxIDMuNTUyMzYgNC42NTYwMUMzLjU1MjM2IDQuNjU2MDEgNC4yOTYwNyA1LjkxMzQ0IDQuNTk4NTUgNi41OTg0QzQuOTAxMDQgNy4yODMzNyA1LjE4MjI3IDguMDgwODMgNS4xMTIyOCA4LjE3ODMyQzUuMDQ4NTMgOC4yNjU4MiA0LjU5NzMgOC4yMDcwNyA0LjU1MjMxIDguMTk5NTdaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik00Ljk2MTIxIDYuMDA0NjVDNC45MTM3MiA2LjE4MDg5IDUuMTQyNDUgNi45ODA4NSA1LjI4MzcgNy41NTQ1N0M1LjQyNDk0IDguMTI4MjkgNS41OTYxOCA4LjczMiA1LjU5NjE4IDguNzMyTDYuMDc4NjUgOS43MTgyTDYuNjEyMzggOS41MjY5NkM2LjYxMjM4IDkuNTI2OTYgNi4zMzExNCA4Ljk4MzI0IDYuMTY5OSA4LjQyMDc3QzYuMDA4NjYgNy44NTU4IDUuNzk5OTIgNi4xMDMzOSA1LjU4NDkzIDUuODc0NjVDNS40MzQ5NCA1LjcxMzQxIDQuOTkyNDYgNS44OTQ2NSA0Ljk2MTIxIDYuMDA0NjVaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik02LjYzMjMyIDUuODAzNDhDNi41MDYwNyA1Ljg4NTk4IDYuNzAyMzEgNy4yMjIxNiA2LjgwMzU2IDcuNzc1ODhDNi45MDQ4IDguMzI5NiA3LjExNjA0IDguOTEzMzIgNy4yMjYwNCA5LjE4NDU2QzcuMzM3MjggOS40NTU3OSA3LjY1ODUxIDEwLjE0MDggNy42NTg1MSAxMC4xNDA4QzcuNjU4NTEgMTAuMTQwOCA3Ljk5MSAxMC41MzMyIDguMDAxIDEwLjQ5MzJDOC4wMTA5OSAxMC40NTMyIDguMTUyMjQgOS45NTk1MSA4LjE1MjI0IDkuOTU5NTFDOC4xNTIyNCA5Ljk1OTUxIDcuNzA4NTEgOC42NTA4MyA3LjYzNzI2IDguMDg3MTFDNy41NjcyNyA3LjUyMzM5IDcuNDU2MDIgNS44MTM0OCA3LjM0NjAzIDUuNzUzNDlDNy4yMzYwNCA1LjY5MzQ5IDYuODMzNTYgNS42NzM0OSA2LjYzMjMyIDUuODAzNDhaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik04Ljg3NTgyIDUuODMzNDRDOC43MjQ1OCA1Ljk3MzQzIDguNTc0NTkgNy44MjU4NCA4LjUxMzM0IDguNTA5NTVDOC40NTIxIDkuMTkzMjYgOC40MDIxIDEwLjE5OTUgOC40MDIxIDEwLjE5OTVMOS4wNzU4MSAxMC4xNDk1QzkuMDc1ODEgMTAuMTQ5NSA5LjMyNzA1IDcuNjY0NTkgOS4zNjcwNSA3LjM0MjExQzkuNDA3MDUgNy4wMTk2MyA5LjU4ODI5IDYuMjg1OTIgOS40OTgyOSA1Ljk5MzQzQzkuNDA4MyA1LjcwMzQ1IDkuMDA1ODIgNS43MTM0NSA4Ljg3NTgyIDUuODMzNDRaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik00LjM1MjMzIDguNDI4MzRDNC4zMDk4MyA4LjUyNDU5IDUuNjIzNTEgMTUuNzEzIDUuNjQ0NzYgMTUuODIwNUM1LjY2NjAxIDE1LjkyNzkgNS42MjM1MSAxNi4wMjU0IDUuODI3MjUgMTYuMTIxN0M2LjAzMjI0IDE2LjIxNzkgNi4zNTIyMiAxNi4yNjkyIDYuNTU3MjEgMTYuMjkxN0M2Ljc2MjIgMTYuMzEyOSA4LjE3MzM4IDE2LjMxNTQgOC4xODQ2MyAxNi4xNDQyQzguMTk1ODcgMTUuOTcxNyAxMi40MTQ0IDkuODI3MDIgMTIuNDE0NCA5LjgyNzAyQzEyLjQxNDQgOS44MjcwMiAxMy4xOTE5IDguNTIzMzQgMTMuMTM4MSA4LjQxNTg0QzEzLjA4NDQgOC4zMDgzNSAxMi44NzQ0IDguMzU4MzUgMTIuNjQ1NiA4LjUxNDU5QzEyLjQxNjkgOC42NzIwOCAxMi4yMDU3IDkuMDYwODEgMTEuNTYzMiA5LjQ5MjA0QzEwLjg2NyA5Ljk1OTUxIDkuOTQ0NTMgMTAuMDU4MyA5LjAxNzA4IDEwLjA0N0M4LjE2MjEzIDEwLjAzNTggNy4xNDU5MyA5Ljg4MDc3IDYuNDE4NDcgOS4zNDIwNEM1Ljc3ODUgOC44NjcwNyA1LjU4OTc2IDguNTI0NTkgNS4wMTk3OSA4LjM3MzM1QzQuNTE3MzIgOC4yMzk2IDQuMzUyMzMgOC40MjgzNCA0LjM1MjMzIDguNDI4MzRaIiBmaWxsPSIjRkQ3MDNGIi8+CjxwYXRoIGQ9Ik01LjM0NjE5IDguODk4MjVMNi41Mzg2MyAxNi4yOTQxQzYuNTM4NjMgMTYuMjk0MSA3Ljk1OTggMTYuNDk5MSA5LjA2NzI1IDE2LjUyMDNDMTAuMTc1OSAxNi41NDE2IDExLjQ5NzEgMTYuMzEwNCAxMS40OTcxIDE2LjMxMDRMMTIuNDU3MSAxMy4wNjU1TDEzLjI3MiA4Ljc4NDVDMTMuMjcyIDguNzg0NSAxMy4wNzA4IDguNTgzMjYgMTIuODQ0NSA4Ljc0OTVDMTIuNjcwOCA4Ljg3NyAxMi4xMzQ2IDkuNzA2OTUgMTEuMTg3MSAxMC4wNzMyQzEwLjExODQgMTAuNDg1NyA4LjE5MjI5IDEwLjUxNTcgNi45ODk4NSAxMC4wMDgyQzUuODg3NDEgOS41NDQ0NiA1LjU1MTE4IDguOTQwNzQgNS4zNDYxOSA4Ljg5ODI1WiIgZmlsbD0iI0ZGMkEyMyIvPgo8cGF0aCBkPSJNMTIuMTg4MyAxNi4wODI5QzExLjk0NTggMTYuMjI3OSAxMS4zODA5IDE2LjMyNTMgMTEuMzgwOSAxNi4zMjUzQzExLjM4MDkgMTYuMzI1MyAxMS45NDA4IDEzLjYwMyAxMi4yNTIxIDEyLjE4MThDMTIuNTYzMyAxMC43NjA2IDEzLjAxMiA4LjM1NzAxIDEzLjAxMiA4LjM1NzAxQzEzLjAxMiA4LjM1NzAxIDEzLjE5MiA4LjMyODI3IDEzLjM3NyA4LjM1MDc2QzEzLjU4NyA4LjM3NzAxIDEzLjcxMiA4LjQzODI2IDEzLjc1NDUgOC41MDQ1MUMxMy44MjMyIDguNjEyIDEzLjQwOTUgMTAuNjE5NCAxMy4wMDQ1IDEyLjU3OTNDMTIuNjM0NSAxNC4zNzkyIDEyLjMxNzEgMTYuMDA2NiAxMi4xODgzIDE2LjA4MjlaIiBmaWxsPSIjREMwRDI1Ii8+CjxkZWZzPgo8cmFkaWFsR3JhZGllbnQgaWQ9InBhaW50MF9yYWRpYWxfNzE4Xzg4IiBjeD0iMCIgY3k9IjAiIHI9IjEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0idHJhbnNsYXRlKDkuMDcyOSAwLjAxNTk3MDYpIHNjYWxlKDEwLjM5MDUgMTAuMzkwNSkiPgo8c3RvcCBvZmZzZXQ9IjAuNDQ3IiBzdG9wLWNvbG9yPSIjRkNDMjE2Ii8+CjxzdG9wIG9mZnNldD0iMC41ODk3IiBzdG9wLWNvbG9yPSIjRkFCQjE2Ii8+CjxzdG9wIG9mZnNldD0iMC43OTM5IiBzdG9wLWNvbG9yPSIjRjNBODE0Ii8+CjxzdG9wIG9mZnNldD0iMC45MjEyIiBzdG9wLWNvbG9yPSIjRUQ5OTEzIi8+CjwvcmFkaWFsR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIuNzM4NTcgMTIuMjMzMUMyLjczODU3IDEyLjIzMzEgMi42MTg1OCAxMy4wMTgxIDIuODk3MzIgMTMuNzEwNUMzLjEyOTggMTQuMjkwNSAzLjczMjI3IDE1LjA1NDIgNC44ODQ3MSAxNS42MzY3QzYuMDM3MTUgMTYuMjE5MSA3LjM2MDgzIDE2LjQyNDEgOS4zMTQ0OCAxNi40NjQxQzExLjI2ODEgMTYuNTA1NCAxMy4yNzU1IDE2LjA0OTIgMTQuMjM5MiAxNS4yMDNDMTUuMjA0MiAxNC4zNTU1IDE1LjU0MTYgMTMuNjQzIDE1LjUxNDIgMTIuODY5M0MxNS40ODY3IDEyLjA5NTYgMTUuMDc1NCAxMS44MDE5IDE1LjA3NTQgMTEuODAxOUwyLjczODU3IDEyLjIzMzFaIiBmaWxsPSIjRkNCNzQ1Ii8+CjxwYXRoIGQ9Ik0zLjI5NzQgMTEuNDQ0M0wyLjcyNzQzIDEyLjAxNDNDMi43Mjc0MyAxMi4wMTQzIDIuNTcyNDQgMTIuNzc2OCAzLjQzMjQgMTMuNjAxN0M0LjA4MzYxIDE0LjIyNTQgNS4yMzYwNSAxNS4zMDkxIDkuMDYzMzUgMTUuMzM3OUMxMi42NDU3IDE1LjM2NTQgMTMuNzcxOSAxNC41Nzc5IDE0LjQ2MzEgMTQuMDIxN0MxNS4xNTU1IDEzLjQ2NTUgMTUuNDE1NSAxMi40OTkzIDE1LjM0NjggMTIuMjI4QzE1LjI3OTMgMTEuOTU2OCAxNS4wNjY4IDExLjgwMDYgMTUuMDY2OCAxMS44MDA2TDMuMjk3NCAxMS40NDQzWiIgZmlsbD0iI0U3OEIyMCIvPgo8cGF0aCBkPSJNMy41NDExMyAxMi4yNTgxQzMuNTQxMTMgMTIuMjU4MSAzLjM3OTg5IDEyLjUzMTkgMy40MzIzOCAxMi43MDU2QzMuNDczNjMgMTIuODQxOSAzLjU1NDg4IDEzLjM5ODEgNS4xNjg1NCAxNC4xMDNDNi4wMjIyNSAxNC40NzY4IDcuNDc0NjcgMTQuNzgxOCA5LjM2MDgyIDE0LjcyNjhDMTEuNTk4MiAxNC42NjE4IDEzLjM5MDYgMTQuMjExOCAxNC4yNTgxIDEzLjM5NjhDMTUuMTQ2OCAxMi41NjMxIDE0Ljk3NjggMTIuMDY2OSAxNC45NzY4IDEyLjA2NjlMMy41NDExMyAxMi4yNTgxWiIgZmlsbD0iI0ZGRDI5MCIvPgo8cGF0aCBkPSJNMy4wNzk3OSAxMC4zNTgyQzMuMDc5NzkgMTAuMzU4MiAyLjU0NDgyIDEwLjkwMzEgMi40NTYwNyAxMS4xNDU2QzIuMzcyMzMgMTEuMzc0NCAyLjM4MzU4IDExLjcwNjggMi41MzEwNyAxMS45NzU2QzIuNzI5ODEgMTIuMzM1NSAyLjk3NzMgMTIuMzczIDMuMjgzNTMgMTIuNTU2OEMzLjYxNDc2IDEyLjc1NDMgNC4xMzg0OSAxMy4zNzA1IDUuNDI3MTcgMTMuNjQxN0M2LjcxNTg1IDEzLjkxMyA4LjQxMjAxIDE0LjAzNTUgOS4zMjA3MSAxNC4wMzU1QzEwLjIyOTQgMTQuMDM1NSAxMS40MzY5IDE0LjAyMTcgMTIuNTYzIDEzLjY0MTdDMTMuNjg5MiAxMy4yNjE4IDE0Ljk3NzkgMTIuNTE1NSAxNS4yNzY2IDExLjg1MDZDMTUuNTc1NCAxMS4xODU2IDE1LjI5MDQgMTAuNzkxOSAxNS4yOTA0IDEwLjc5MTlMMTQuNjM5MiAxMC4zNTgySDMuMDc5NzlaIiBmaWxsPSIjNkQ1NDREIi8+CjxwYXRoIGQ9Ik0yLjc2NzMyIDkuNjY2OTNDMi43NjczMiA5LjY2NjkzIDIuMzUyMzUgOS42NDgxOCAyLjI1MjM1IDEwLjI3NjlDMi4xNzk4NiAxMC43MzQ0IDIuNTA5ODQgMTEuMDIzMSAyLjUwOTg0IDExLjAyMzFDMi41MDk4NCAxMS4wMjMxIDIuMzM0ODUgMTEuMjc0MyAyLjUzNzM0IDExLjU5MzFDMi43MjczMyAxMS44OTE4IDIuOTMxMDcgMTEuOTU5MyAzLjM1MTA0IDEyLjAyNjhDMy42NTM1MyAxMi4wNzU2IDMuODEyMjcgMTEuOTg1NiA0LjIwNiAxMi4yMDNDNC42OTk3MiAxMi40NzU1IDQuOTEwOTYgMTIuOTYzIDUuNTg5NjggMTMuMTExN0M2LjI2ODM5IDEzLjI2MDUgNy4wMjgzNSAxMy4xMjU1IDcuMDU0NiAxMy4wMTY4QzcuMDgwODUgMTIuOTA4IDYuMTA0NjUgMTIuNTAxOCA0LjM0MDk5IDExLjUxMDZDMy43NDYwMiAxMS4xNzY5IDMuMjAxMDUgMTAuNzIzMSAzLjA1MjMxIDEwLjQ5MzFDMi45MDM1NyAxMC4yNjMxIDIuNzY3MzIgOS42NjY5MyAyLjc2NzMyIDkuNjY2OTNaIiBmaWxsPSIjOUI3MTY5Ii8+CjxwYXRoIGQ9Ik0xMS4zMDIgMTIuOTc2OEMxMS4zMDIgMTMuMTUzMSAxMS44ODU3IDEzLjA3MTggMTIuMTU3IDEyLjkyMzFDMTIuNDI4MiAxMi43NzQzIDEyLjg2MTkgMTIuMzk0MyAxMy4yNTU2IDEyLjIzMDZDMTMuNjQ5NCAxMi4wNjgxIDEzLjc5ODEgMTIuMjE2OSAxNC4zNjgxIDExLjk0NTZDMTQuOTM4MSAxMS42NzQ0IDE1LjI1MDUgMTEuMDM2OSAxNS4yNTA1IDExLjAzNjlDMTUuMjUwNSAxMS4wMzY5IDE1LjUyOCAxMC42OTQ0IDE1LjU1MDUgMTAuNDU0NEMxNS41ODU1IDEwLjA4ODIgMTUuNTQ5MyA5LjkxMDcyIDE1LjI5MTggOS44Mjk0OEMxNS4wMzQzIDkuNzQ4MjMgMTQuNDczMSAxMC41OTE5IDE0LjM5MTggMTAuNjg2OUMxNC4zMTA2IDEwLjc4MTkgMTMuMTA4MiAxMS42NjA2IDEyLjYzMzIgMTEuOTk5NEMxMi4xNTgyIDEyLjMzODEgMTEuMzAyIDEyLjg4MTggMTEuMzAyIDEyLjk3NjhaIiBmaWxsPSIjOUI3MTY5Ii8+CjxwYXRoIGQ9Ik0zLjI5NzMzIDkuMjA1NzdDMy4yOTczMyA5LjIwNTc3IDIuNjE4NjIgOS4yODcwMiAyLjUyMzYyIDkuNTcyQzIuNDI4NjMgOS44NTY5OSAyLjcyNzM2IDEwLjI3NyAzLjUxMzU3IDEwLjgwNjlDNC4xNTg1MyAxMS4yNDA3IDYuMzY4NDIgMTIuMjg0NCA3LjI0NDYyIDEyLjgyODFDOC4yNDgzMiAxMy40NTE4IDkuMDk4MjcgMTQuMzIzIDkuMjM5NTIgMTQuMzM0M0M5LjQxNTc2IDE0LjM0OCAxMC42Mzk0IDEzLjA0ODEgMTIuMzMzMSAxMS45MDU2QzEzLjk0MyAxMC44MTgyIDE1LjA0NjcgMTAuNTQ5NCAxNS40NDA0IDkuNzQ4MjRDMTUuNjUxNyA5LjMxODI2IDE1LjE0MTcgOS4wNjk1MyAxNS4xNDE3IDkuMDY5NTNMOC42MDIwNSA2LjU4NzE2TDMuMjk3MzMgOS4yMDU3N1oiIGZpbGw9IiNGRkU4NjkiLz4KPHBhdGggZD0iTTkuMzIwODggMTMuNjAxOEM5LjUyMzM3IDEzLjU3OTMgMTAuNTgyMSAxMi41NTY4IDExLjgzMDcgMTEuNzcwNkMxMy4wNzk0IDEwLjk4NDQgMTQuOTI0MyAxMC4xNDIgMTUuMTk1NiA5LjY2ODIzQzE1LjQ2NjggOS4xOTQ1IDE0LjgxNTYgOC44ODA3NyAxNC44MTU2IDguODgwNzdMNy4yMTcyNCA3LjYwNTgzTDIuNzY3NDcgOS4zNTU3NEMyLjc2NzQ3IDkuMzU1NzQgMi41MjM3NCA5LjQzNjk5IDIuNjg2MjMgOS42NDA3M0MyLjg0ODcyIDkuODQ0NDcgMy4xMDYyMSAxMC4xODMyIDMuNjc2MTggMTAuNTM1N0M0LjM2ODY0IDEwLjk2NDQgNi4xNDYwNCAxMS42NzU2IDcuMjcwOTkgMTIuMjg1NkM4LjM5ODQzIDEyLjg5NTYgOS4wNzcxNCAxMy42MjggOS4zMjA4OCAxMy42MDE4WiIgZmlsbD0iI0ZGQkUwMSIvPgo8cGF0aCBkPSJNMy4zNzk4OCA5LjU1ODI3TDMuNjE2MTIgMTAuMTkwN0MzLjYxNjEyIDEwLjE5MDcgNS4zMTYwMyAxMS45MDQ0IDguNjk3MSAxMS45NzE5QzEyLjA2MTkgMTIuMDM5NCAxMy45NDgxIDEwLjQzODIgMTMuOTQ4MSAxMC40MzgyTDE0LjM3ODEgOS4zNTgyOEwzLjM3OTg4IDkuNTU4MjdaIiBmaWxsPSIjRjQ0ODJCIi8+CjxwYXRoIGQ9Ik03LjgxMjIyIDEwLjYwMzFMNy41NDU5OCAxMS4wMjk0QzcuNTQ1OTggMTEuMDI5NCA4LjExNDcgMTEuMzcwNiA4LjI3ODQ0IDExLjQyOTRDOC41MTM0MyAxMS41MTMxIDguNzQ0NjcgMTEuNDgzMSA5LjA5MDkgMTEuMzc1NkM5LjQzNzEzIDExLjI2OTQgMTAuODc0NiAxMC42OTY5IDEwLjg3NDYgMTAuNjk2OUMxMC44NzQ2IDEwLjY5NjkgMTIuMDYzMiAxMC43MTQ0IDEyLjgwOTUgMTAuNzI4MUMxMy40NzA3IDEwLjc0MDYgMTQuMTMzMSAxMC41MDMyIDE0LjQ3OTQgMTAuMTAzMkMxNC44MjU2IDkuNzAzMiAxNS4zNjMxIDguODQ1NzQgMTUuMzYzMSA4Ljg0NTc0QzE1LjM2MzEgOC44NDU3NCAxNS42NDMxIDguNzc5NSAxNS44MjkzIDguNTY1NzZDMTYuMDE1NSA4LjM1MjAyIDE2LjAyOTMgOC4xNjU3OCAxNi4wMjkzIDguMTY1NzhDMTYuMDI5MyA4LjE2NTc4IDE1Ljk3NTUgNy44MDU4IDE1Ljc3NjggNy43MTMzQzE1LjU3NjggNy42MTk1NiA2LjY2OTc4IDcuMDc0NTggNi42Njk3OCA3LjA3NDU4TDMuMTAxMjIgNy4zNDA4MkwyLjUgOC41Nzk1MUw0LjQ4MzY1IDEwLjI3MDdMNy4yNzk3NSAxMC40MzA3TDcuODEyMjIgMTAuNjAzMVoiIGZpbGw9IiNBNkI3MzIiLz4KPHBhdGggZD0iTTIuNjg2MTQgNy40ODgzN0MyLjQ4NDkgNy4zOTcxMyAyLjI3MzY2IDcuNDA4MzggMi4xNzk5MiA3LjY3NDYxQzIuMDg2MTcgNy45NDA4NSAyLjExNDkyIDguMDU4MzQgMi4xMTQ5MiA4LjA1ODM0QzIuMTE0OTIgOC4wNTgzNCAxLjg0MTE4IDguMTE1ODQgMS44MDc0NCA4LjQ1OTU3QzEuNzY3NDQgOC44NzIwNSAyLjIxOTkxIDkuMDE4MjkgMi4yMTk5MSA5LjAxODI5QzIuMjE5OTEgOS4wMTgyOSAyLjI0MTE2IDkuMTc4MjggMi40MTk5IDkuMzM4MjhDMi42MjM2NCA5LjUyMDc3IDIuODI3MzggOS41MDIwMiAyLjgyNzM4IDkuNTAyMDJDMi44MjczOCA5LjUwMjAyIDMuMzY0ODUgMTAuMDMwNyAzLjcyNDgzIDEwLjI0MzJDNC4wODQ4MiAxMC40NTU3IDQuNjAzNTQgMTAuNjAzMiA1LjEyMzUxIDEwLjU4OTVDNS42NDM0OCAxMC41NzU3IDUuODE1OTcgMTAuMzM3IDYuMjAyMiAxMC40Njk1QzYuNTg4NDMgMTAuNjAyIDcuNjEyMTMgMTEuMTQwNyA4LjAyNDYxIDExLjE0OTRDOC42NzcwNyAxMS4xNjMyIDEwLjA4ODIgMTAuMTUwNyAxMC40MDgyIDEwLjE2NDVDMTAuNzI4MiAxMC4xNzgyIDEyLjE3OTQgMTAuNDA0NSAxMi45NTE4IDEwLjQwNDVDMTMuNzI0MyAxMC40MDQ1IDE0LjAwNDMgMTAuMTc4MiAxNC4xNzY4IDkuOTUxOTlDMTQuMzQ5MyA5LjcyNTc2IDE0LjY0MyA5LjE1MzI5IDE0LjU0OTMgOS4xMzk1NEMxNC40NTU1IDkuMTI1NzkgMTMuODQzIDkuNjMyMDEgMTMuNDA0MyA5LjU5MjAxQzEyLjk2NDMgOS41NTIwMSAxMi41Nzk0IDkuMjk5NTMgMTIuMDMzMSA5LjI5OTUzQzExLjQ4NjkgOS4yOTk1MyA4LjgyNDU2IDkuNjQ1NzYgOC42NTA4MiA5LjcyNTc2QzguNDc3MDggOS44MDU3NSA3LjczMjEyIDEwLjM3ODIgNy41ODU4OCAxMC4zNTJDNy40Mzk2NCAxMC4zMjU3IDYuOTMzNDEgOS44OTk1IDYuNzYwOTIgOS44MDU3NUM2LjU4ODQzIDkuNzEyMDEgNC43NzIyOCA5LjY1MjAxIDQuNTExMDQgOS42OTk1MUM0LjMyMzU1IDkuNzMzMjYgNC4wNTg1NyA5Ljc3OTUgNC4wNTg1NyA5Ljc3OTVDNC4wNTg1NyA5Ljc3OTUgNC4wODQ4MiA5LjM2NzAyIDMuODU4NTggOS4xMjcwNEMzLjYzMjM0IDguODg3MDUgMi44ODYxMyA4LjQ3NDU3IDIuODg2MTMgOC40NzQ1N0MyLjg4NjEzIDguNDc0NTcgMi45Nzg2MiA3LjYyMDg3IDIuNjg2MTQgNy40ODgzN1oiIGZpbGw9IiNDMkREMUYiLz4KPHBhdGggZD0iTTE0LjQ5NTYgOC4yMDcwN0MxNC40OTU2IDguMjA3MDcgMTUuMzA4MSA4LjIwNzA3IDE1LjUzNDMgOC4xOTMzMkMxNS43NjA1IDguMTc5NTcgMTYuMDI2OCA4LjE2NzA3IDE2LjAyNjggOC4xNjcwN0MxNi4wMjY4IDguMTY3MDcgMTUuOTQ5MyA3Ljc5ODM0IDE1LjgzMTggNy42MDIxQzE1LjU5MTggNy4xOTk2MiAxNS4wMTQzIDcuMDg4MzggMTUuMDE0MyA3LjA4ODM4TDE0LjQ5NTYgOC4yMDcwN1oiIGZpbGw9IiNDMkREMUYiLz4KPHBhdGggZD0iTTIuNDk5ODggNi45OTU4OEwyLjQxMTEzIDcuMzg4MzZDMi40MTExMyA3LjM4ODM2IDMuNjM5ODIgOS41NjQ0OSA5LjE2MjAzIDkuNjQ2OTlDMTIuMzk0NCA5LjY5NTc0IDE0LjI1MTggOC43MTQ1NCAxNS4wNzE3IDcuOTEyMDhDMTUuNTYyOSA3LjQzMDg1IDE1LjU3OTIgNi45NjMzOCAxNS41NzkyIDYuOTYzMzhMMi40OTk4OCA2Ljk5NTg4WiIgZmlsbD0iI0RFODAxMCIvPgo8cGF0aCBkPSJNMi4yMjAwMiA1LjkxMzQyQzIuMTUwMDMgNy4wMDA4NiAyLjQxNTAxIDcuMzkwODQgMi40MTUwMSA3LjM5MDg0QzIuNDE1MDEgNy4zOTA4NCA0LjI0MjQyIDkuMDQ1NzUgOC45MDk2NyA5LjEyMkMxMy42OTMyIDkuMTk5NSAxNS4yNDgxIDcuNjEzMzMgMTUuNTIwNiA3LjE1MDg1QzE1LjY5NDMgNi44NTcxMiAxNS44MzMxIDYuMjgwOSAxNS43MjMxIDUuNDQwOTRDMTUuNTQwNiA0LjA1MzUyIDEzLjk0MzIgMS40MjExNSA4Ljg4NzE3IDEuNDQxMTVDMy44MzExOSAxLjQ2MTE1IDIuMzIyNTIgNC4zMzQ3NSAyLjIyMDAyIDUuOTEzNDJaIiBmaWxsPSJ1cmwoI3BhaW50MF9yYWRpYWxfNzIxXzE4NCkiLz4KPHBhdGggZD0iTTcuMDIzNSAyLjc5NDg0QzcuMDYyMjUgMy4wMDczMyA2LjEwMzU1IDMuNzE0OCA1Ljk2NDgxIDMuMDM2MDhDNS44MzQ4MiAyLjM5NzM2IDYuOTg3MjUgMi41OTg2IDcuMDIzNSAyLjc5NDg0WiIgZmlsbD0iI0ZERUFDOCIvPgo8cGF0aCBkPSJNNy4zNjU4NiA0LjEyNjA1QzcuNDE1ODYgNC4yNTcyOSA4LjM5ODMxIDQuNDQ2MDMgOC4yOTMzMSAzLjg5MjMxQzguMTgzMzIgMy4zMTYwOSA3LjI2MzM3IDMuODU2MDYgNy4zNjU4NiA0LjEyNjA1WiIgZmlsbD0iI0ZERUFDOCIvPgo8cGF0aCBkPSJNOC4yMjcwOSAyLjYwNjA0QzguMjUyMDkgMi43OTQ3OCA5LjM5NzAzIDMuMDE0NzcgOS4zNzMyOCAyLjQ4MjNDOS4zNDQ1NCAxLjgyNDgzIDguMTk4MzUgMi4zODczIDguMjI3MDkgMi42MDYwNFoiIGZpbGw9IiNGREVBQzgiLz4KPHBhdGggZD0iTTYuMzM4NDYgNC41NjM1MkM2LjM0NDcxIDQuNjA2MDIgNi4wNjM0OCA0LjgyNzI1IDUuNzc1OTkgNC45MTM1QzUuNTIzNSA0Ljk4OTc1IDUuMjY0NzcgNC45NTcyNSA1LjIzNjAyIDQuNjQzNTFDNS4yMDg1MiA0LjM0MTAzIDUuNDIzNTEgNC4yMzEwNCA1LjY4MSA0LjI1NzI4QzUuOTc3MjMgNC4yODYwMyA2LjMyNTk2IDQuNDgyMjcgNi4zMzg0NiA0LjU2MzUyWiIgZmlsbD0iI0ZERUFDOCIvPgo8cGF0aCBkPSJNOC43MTcwNSA1LjQ0NzI0QzguNjY5NTUgNS4yNDg1IDcuNTkzMzYgNC45NzIyNiA3LjY2NTg2IDUuNjAwOThDNy43Mzk2IDYuMjI3MiA4Ljc2ODMgNS42NTg0OCA4LjcxNzA1IDUuNDQ3MjRaIiBmaWxsPSIjRkRFQUM4Ii8+CjxwYXRoIGQ9Ik05LjcyNDQ4IDQuMDE2QzkuNjcxOTggMy45NTQ3NSA5LjQyNTc0IDMuOTY2IDkuMTI1NzYgNC4wNDQ3NUM4LjkwOTUyIDQuMTAyMjQgOC43NDk1MyA0LjM0NTk4IDguODk5NTIgNC41NzcyMkM5LjA3NDUxIDQuODQ3MjEgOS4zNjgyNCA0LjczMzQ2IDkuNTU1NzMgNC41MDQ3MkM5LjcwODIzIDQuMzE5NzMgOS43ODMyMiA0LjA4NDc1IDkuNzI0NDggNC4wMTZaIiBmaWxsPSIjRkRFQUM4Ii8+CjxwYXRoIGQ9Ik0xMC44NzcxIDIuNTU0ODJDMTAuODQzNCAyLjUxODU4IDEwLjU5NzEgMi40OTM1OCAxMC4zOTU5IDIuNTkxMDdDMTAuMTkzNCAyLjY4ODU3IDEwLjAxODQgMi45MjEwNiAxMC4yMDU5IDMuMTM4NTRDMTAuMzk1OSAzLjM1NzI4IDEwLjY3ODQgMy4yMzEwNCAxMC44MTIxIDIuOTk5OEMxMC45MTg0IDIuODEzNTYgMTAuOTI4NCAyLjYwOTgyIDEwLjg3NzEgMi41NTQ4MloiIGZpbGw9IiNGREVBQzgiLz4KPHBhdGggZD0iTTExLjY0OTUgNC40NTcyN0MxMS42MTcgNC4yNTM1MyAxMC43MTIgMy44OTEwNSAxMC43MTgzIDQuNDQ2MDJDMTAuNzI0NSA1LjAxNDc0IDExLjY3ODIgNC42NDEwMSAxMS42NDk1IDQuNDU3MjdaIiBmaWxsPSIjRkRFQUM4Ii8+CjxwYXRoIGQ9Ik0xMi45MDA1IDMuNDUyMjhDMTIuODcwNSAzLjI5NjA0IDExLjcxOTMgMy4xNjk3OSAxMS45MTU2IDMuNzUxMDFDMTIuMTE5MyA0LjM1NzIzIDEyLjkzNjggMy42NDIyNyAxMi45MDA1IDMuNDUyMjhaIiBmaWxsPSIjRkRFQUM4Ii8+CjxwYXRoIGQ9Ik0xMS4zMTU3IDUuNDk3MTRDMTEuMzIzMiA1LjMyMjE1IDEwLjQ0NyA0LjkyMDkyIDEwLjM4MiA1LjQ0NTg5QzEwLjMxNyA1Ljk3MDg3IDExLjMwNTcgNS43MjQ2MyAxMS4zMTU3IDUuNDk3MTRaIiBmaWxsPSIjRkRFQUM4Ii8+CjxkZWZzPgo8cmFkaWFsR3JhZGllbnQgaWQ9InBhaW50MF9yYWRpYWxfNzIxXzE4NCIgY3g9IjAiIGN5PSIwIiByPSIxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgZ3JhZGllbnRUcmFuc2Zvcm09InRyYW5zbGF0ZSg4LjkzMjggMi45NDAxOCkgcm90YXRlKDg5LjM1ODMpIHNjYWxlKDUuNTA0ODEgOS41OTE5OCkiPgo8c3RvcCBvZmZzZXQ9IjAuNTE3IiBzdG9wLWNvbG9yPSIjREY4MDE2Ii8+CjxzdG9wIG9mZnNldD0iMC42NDI2IiBzdG9wLWNvbG9yPSIjRTU4QzIxIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0Y1QUMzQyIvPgo8L3JhZGlhbEdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNTMxNDggMTUuMDA0OEM0LjAzMjk3IDE0LjkwNzUgNC4zMjM4NCAxNC4yMzI2IDQuMTgxMTUgMTMuNDk3M0M0LjAzODQ3IDEyLjc2MiAzLjUxNjI2IDEyLjI0NDggMy4wMTQ3OCAxMi4zNDIxQzIuNTEzMjkgMTIuNDM5NCAyLjIyMjQyIDEzLjExNDQgMi4zNjUxIDEzLjg0OTdDMi41MDc3OSAxNC41ODUgMy4wMjk5OSAxNS4xMDIxIDMuNTMxNDggMTUuMDA0OFoiIGZpbGw9IiM2MTYxNjEiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yLjgwNTAxIDEzLjc3NDNDMi41OTc2MSAxMi4wMDgyIDIuNDcwMjQgMTAuMTU5MiAyLjU1MTY2IDguNDg2MzNMMy4zMDA3MyA4LjUyMjc4QzMuMjIyMTYgMTAuMTM3MyAzLjM0NDc4IDExLjk0MDUgMy41NDk4NSAxMy42ODY4TDIuODA1MDEgMTMuNzc0M1oiIGZpbGw9IiM2MTYxNjEiLz4KPHBhdGggZD0iTTE1LjYzMjcgMTMuODQ4MkMxNS43NzU0IDEzLjExMjkgMTUuNDg0NSAxMi40Mzc5IDE0Ljk4MyAxMi4zNDA2QzE0LjQ4MTUgMTIuMjQzMyAxMy45NTkzIDEyLjc2MDUgMTMuODE2NiAxMy40OTU4QzEzLjY3NCAxNC4yMzExIDEzLjk2NDggMTQuOTA2IDE0LjQ2NjMgMTUuMDAzNEMxNC45Njc4IDE1LjEwMDcgMTUuNDkgMTQuNTgzNSAxNS42MzI3IDEzLjg0ODJaIiBmaWxsPSIjNjE2MTYxIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTQuNDQ5NCAxMy42ODY4QzE0LjY1NDUgMTEuOTQwNSAxNC43NzcxIDEwLjEzNzMgMTQuNjk4NSA4LjUyMjc4TDE1LjQ0NzYgOC40ODYzM0MxNS41MjkgMTAuMTU5MiAxNS40MDE2IDEyLjAwODIgMTUuMTk0MiAxMy43NzQzTDE0LjQ0OTQgMTMuNjg2OFoiIGZpbGw9IiM2MTYxNjEiLz4KPHBhdGggZD0iTTE1LjI0MDUgOS4zMjU3MUMxNC41OTQzIDkuMjk1NzIgMTQuMDU4MSA4LjgxMDc0IDEzLjk3NDMgOC4xNjk1M0MxMy44ODA2IDcuNDQzMzEgMTMuNjM5NCA2LjYwNDYxIDEzLjAzNDQgNS43OTk2NUMxMi4wODMyIDQuNTM1OTcgMTAuNjkwOCAzLjg2NzI1IDkuMDA1ODYgMy44NjcyNUM3LjgyNDY3IDMuODY3MjUgNi4xNjQ3NiA0LjIwMjI0IDQuOTYzNTcgNS43OTk2NUM0LjM1MTEgNi42MTQ2MSA0LjExMjM3IDcuNDcwODEgNC4wMjExMiA4LjIwODI3QzMuOTQ0ODggOC44MTk0OSAzLjQ0OTkgOS4yOTMyMiAyLjgzMzY4IDkuMzIxOTdMMi43NTg2OSA5LjMyNTcxQzIuMzI2MjEgOS4zNDU3MSAxLjk2ODczIDguOTg4MjMgMS45OTM3MyA4LjU1N0MyLjA1MzczIDcuNTAzMzEgMi4zMTg3MSA1LjYzMjE2IDMuMzc0OTEgNC4yMjcyM0M0LjY5NzM0IDIuNDY3MzMgNi42OTcyMyAxLjQ5OTg4IDkuMDA1ODYgMS40OTk4OEMxMS4zMDU3IDEuNDk5ODggMTMuMzAwNiAyLjQ2NzMzIDE0LjYyMzEgNC4yMjQ3M0MxNS42ODA1IDUuNjMwOTEgMTUuOTQ0MiA3LjUwMzMxIDE2LjAwNTUgOC41NTU3NkMxNi4wMzA1IDguOTg4MjMgMTUuNjczIDkuMzQ1NzEgMTUuMjQwNSA5LjMyNTcxWiIgZmlsbD0iIzYxNjE2MSIvPgo8cGF0aCBkPSJNNS44NDEwNCAxNi40MTAzQzQuNjI2MSAxNi41NzkxIDMuMjMyNDMgMTUuNjgyOSAyLjk1MTE5IDEzLjYwOTJDMi42NzEyMSAxMS41NDQzIDMuNjUyNDEgMTAuMjg5NCA0LjY0NzM1IDEwLjA5NjlMNS44NDEwNCAxNi40MTAzWiIgZmlsbD0iIzQyNDI0MiIvPgo8cGF0aCBkPSJNNS45MTEwNSA5LjY2OTQ2TDQuODUzNiA5Ljg1MDdDNC40MjYxMyA5LjkzMzE5IDQuMDkxMTQgMTAuMjY0NCA0LjAwMzY1IDEwLjY5MTlDMy44ODYxNiAxMS4yNjgxIDMuODExMTYgMTIuMTg5MyA0LjA1MzY1IDEzLjQ0NTVDNC4yOTczOCAxNC43MDA0IDQuNzExMTEgMTUuNTI2NiA1LjAzNzM0IDE2LjAxNzlDNS4yNzczMyAxNi4zODA0IDUuNzEyMzEgMTYuNTYyOCA2LjEzOTc5IDE2LjQ3OTFMNy4xOTcyMyAxNi4yOTc5QzcuNzA4NDUgMTYuMTk5MSA3LjgzNDcgMTQuNjM0MiA3LjQ3ODQ3IDEyLjgwNDNDNy4xMjM0OCAxMC45NzQ0IDYuNDIyMjcgOS41NzA3MSA1LjkxMTA1IDkuNjY5NDZaIiBmaWxsPSIjNzU3NTc1Ii8+CjxwYXRoIG9wYWNpdHk9IjAuNSIgZD0iTTcuMzAwOTQgMTYuMDE2MkM3LjcyNzIxIDE1LjkzMzUgNy44MDY3OCAxNC40OTU4IDcuNDc4NjcgMTIuODA0OUM3LjE1MDU3IDExLjExNDEgNi41MzkwMyA5LjgxMDQ4IDYuMTEyNzYgOS44OTMyQzUuNjg2NSA5Ljk3NTkxIDUuNjA2OTMgMTEuNDEzNyA1LjkzNTAzIDEzLjEwNDVDNi4yNjMxNCAxNC43OTUzIDYuODc0NjggMTYuMDk4OSA3LjMwMDk0IDE2LjAxNjJaIiBmaWxsPSIjNDI0MjQyIi8+CjxwYXRoIGQ9Ik03LjIzMDk2IDEyLjg1M0M3LjQ1MzQ1IDEzLjk5NzkgNy41MDIyIDE0Ljk1MTYgNy4zNDA5NSAxNC45ODI5QzcuMTc5NzEgMTUuMDE0MSA2Ljg2NzIzIDE0LjExMTcgNi42NDU5OSAxMi45NjY3QzYuNDIzNSAxMS44MjE4IDYuMzc0NzYgMTAuODY4MSA2LjUzNiAxMC44MzY4QzYuNjk3MjQgMTAuODA1NiA3LjAwODQ3IDExLjcwODEgNy4yMzA5NiAxMi44NTNaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNzE1XzMwMikiLz4KPHBhdGggZD0iTTEyLjE1ODIgMTYuNDEwM0MxMy4zNzMxIDE2LjU3OTEgMTQuNzY2OCAxNS42ODI5IDE1LjA0ODEgMTMuNjA5MkMxNS4zMjggMTEuNTQ0MyAxNC4zNDY4IDEwLjI4OTQgMTMuMzUxOSAxMC4wOTY5TDEyLjE1ODIgMTYuNDEwM1oiIGZpbGw9IiM0MjQyNDIiLz4KPHBhdGggZD0iTTEyLjA4ODIgOS42Njk0NkwxMy4xNDU2IDkuODUwN0MxMy41NzMxIDkuOTMzMTkgMTMuOTA4MSAxMC4yNjQ0IDEzLjk5NTYgMTAuNjkxOUMxNC4xMTMxIDExLjI2ODEgMTQuMTg4MSAxMi4xODkzIDEzLjk0NTYgMTMuNDQ1NUMxMy43MDE4IDE0LjcwMDQgMTMuMjg4MSAxNS41MjY2IDEyLjk2MTkgMTYuMDE3OUMxMi43MjE5IDE2LjM4MDQgMTIuMjg2OSAxNi41NjI4IDExLjg1OTQgMTYuNDc5MUwxMC44MDIgMTYuMjk3OUMxMC4yOTA4IDE2LjE5OTEgMTAuMTY0NSAxNC42MzQyIDEwLjUyMDggMTIuODA0M0MxMC44NzU3IDEwLjk3NDQgMTEuNTc3IDkuNTcwNzEgMTIuMDg4MiA5LjY2OTQ2WiIgZmlsbD0iIzc1NzU3NSIvPgo8cGF0aCBvcGFjaXR5PSIwLjUiIGQ9Ik0xMi4wNjM3IDEzLjEwMzVDMTIuMzkxOCAxMS40MTI3IDEyLjMxMjIgOS45NzQ5NCAxMS44ODYgOS44OTIyMkMxMS40NTk3IDkuODA5NSAxMC44NDgyIDExLjExMzEgMTAuNTIwMSAxMi44MDRDMTAuMTkxOSAxNC40OTQ4IDEwLjI3MTUgMTUuOTMyNSAxMC42OTc4IDE2LjAxNTNDMTEuMTI0MSAxNi4wOTggMTEuNzM1NiAxNC43OTQzIDEyLjA2MzcgMTMuMTAzNVoiIGZpbGw9IiM0MjQyNDIiLz4KPHBhdGggZD0iTTEwLjc2ODMgMTIuODUzQzEwLjU0NTggMTMuOTk3OSAxMC40OTcxIDE0Ljk1MTYgMTAuNjU4MyAxNC45ODI5QzEwLjgxOTYgMTUuMDE0MSAxMS4xMzIgMTQuMTExNyAxMS4zNTMzIDEyLjk2NjdDMTEuNTc1OCAxMS44MjE4IDExLjYyNDUgMTAuODY4MSAxMS40NjMzIDEwLjgzNjhDMTEuMzAyIDEwLjgwNTYgMTAuOTkwOCAxMS43MDgxIDEwLjc2ODMgMTIuODUzWiIgZmlsbD0idXJsKCNwYWludDFfbGluZWFyXzcxNV8zMDIpIi8+CjxwYXRoIGQ9Ik0xNS44ODU1IDguNzYxOThDMTUuNTMxOCA5LjAwMTk3IDE1LjA5NTUgOC43MzE5OCAxNC45NjE4IDguMzY3QzE0LjgwNjggNy40NTcwNSAxNC41MDA2IDYuNDAwODYgMTMuOTAwNiA1LjUyOTY1QzEyLjc3NjkgMy44MDU5OSAxMC44ODgyIDIuOTQyMjkgOC45OTk2IDIuOTQxMDRDNy4xMTA5NSAyLjk0MjI5IDUuMjIyMyAzLjgwNTk5IDQuMDk4NjEgNS41Mjk2NUMzLjQ5ODY0IDYuNDAwODYgMy4xOTM2NSA3LjQ1ODMgMy4wMzc0MSA4LjM2N0MyLjkwMzY3IDguNzMxOTggMi40Njc0NCA5LjAwMTk3IDIuMTEzNzEgOC43NjE5OEMyLjA4NDk2IDguNzQxOTggMi4wNDEyMSA4LjY5MTk5IDEuOTk2MjIgOC42NjQ0OUMyLjAzMTIxIDkuMDQ1NzIgMi4zNjM3IDkuMzQxOTUgMi43NTg2OCA5LjMyNDQ1TDIuODY3NDIgOS4zMTk0NUMzLjYzOTg4IDkuMzI1NyAzLjk0NjExIDguODM0NDggNC4wMTczNiA4LjI0MDc2QzQuMTA2MSA3LjQ5NzA1IDQuMzQyMzQgNi42MjU4NSA0Ljk2NDgxIDUuNzk5NjRDNi4xNjM1IDQuMjA1OTcgNy44MTg0MSAzLjg2ODQ5IDguOTk5NiAzLjg2NzI0QzEwLjE4MDggMy44Njg0OSAxMS44MzU3IDQuMjA1OTcgMTMuMDM0NCA1Ljc5OTY0QzEzLjY0NjkgNi42MTQ2IDEzLjg4NTYgNy40NzA4IDEzLjk3NjggOC4yMDgyNkMxNC4wNTQzIDguODI2OTggMTQuMzU0MyA5LjMyNTcgMTUuMTk4IDkuMzIzMkwxNS4yMzkzIDkuMzI1N0MxNS42MzQyIDkuMzQ0NDUgMTUuOTY2NyA5LjA0Njk3IDE2LjAwMTcgOC42NjU3NEMxNS45NTggOC42OTE5OSAxNS45MTQyIDguNzQxOTggMTUuODg1NSA4Ljc2MTk4WiIgZmlsbD0iIzQyNDI0MiIvPgo8cGF0aCBkPSJNMi44MzEyMSA2LjIwMDkxQzIuNzE0OTcgNi4xMTg0MSAyLjYyMzcyIDYuMDI3MTcgMi43ODk5NiA1LjM5OTdDMi45OTEyIDQuNjQzNDkgMy45MjQ5IDMuMTc5ODIgNS44MDYwNSAyLjIwOTg3QzYuMTU0NzggMi4wMjk4OCA3LjAwMjI0IDEuNzYxMTQgNy4yNjA5OCAyLjIyNzM3QzcuNTA1OTYgMi42Njk4NCA2LjMyMjI4IDIuOTkyMzMgNS45MDQ4IDMuMjE2MDdDNC44MTIzNiAzLjgwMzUzIDQuMDIzNjUgNC42MzIyNCAzLjU5OTkyIDUuMjg3MjFDMy40NjQ5MyA1LjQ5NTk1IDMuMTA4NyA2LjM5NTkgMi44MzEyMSA2LjIwMDkxWiIgZmlsbD0iIzc1NzU3NSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzcxNV8zMDIiIHgxPSI2LjY3OTY5IiB5MT0iMTIuOTYwOCIgeDI9IjcuMDM5MjMiIHkyPSIxMi44OTEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agb2Zmc2V0PSIwLjIwMTYiIHN0b3AtY29sb3I9IiMyMTIxMjEiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMjEyMTIxIiBzdG9wLW9wYWNpdHk9IjAiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDFfbGluZWFyXzcxNV8zMDIiIHgxPSIxMS4zMTk2IiB5MT0iMTIuOTYwOSIgeDI9IjEwLjk2IiB5Mj0iMTIuODkxMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMjAxNiIgc3RvcC1jb2xvcj0iIzIxMjEyMSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyMTIxMjEiIHN0b3Atb3BhY2l0eT0iMCIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1LjkxNTMgMTQuNDA5MkwxNS4xOTY2IDE0LjA5NDNDMTUuMTkxNiAxNC4wNTU1IDE1LjE4NTQgMTQuMDE2OCAxNS4xNzU0IDEzLjk3OTNDMTUuMTIxNiAxMy43NzA1IDE0LjkyNDEgMTMuNTE2OCAxNC41OTc5IDEzLjQ0M0wxNC41MzE2IDEyLjUwNjhMMTMuODMxNyAxMi41NDY4TDEzLjkxOTIgMTMuNTQzQzEzLjY1OTIgMTMuNjg1NSAxMy41MzkyIDEzLjkzNjggMTMuNTQ3OSAxNC4yMjU1TDEyLjgxMyAxNC44MjE3TDEzLjI1MDUgMTUuMzcwNEwxMy44NDE3IDE0Ljg3M0MxMy45ODkyIDE0Ljk4NjcgMTQuMTgyOSAxNS4wODE3IDE0LjQyMjkgMTUuMDY4QzE0LjcxMTYgMTUuMDUxNyAxNC45MDkxIDE0LjkwMTcgMTUuMDMxNiAxNC43NDkyTDE1LjY2NzggMTUuMDkwNUwxNS45MTUzIDE0LjQwOTJaIiBmaWxsPSIjODc4Nzg3Ii8+CjxwYXRoIGQ9Ik02LjAxMzUgMTQuMTM3OUM1Ljg4MjI2IDEyLjQ2NDMgNC41MDIzMyAxMS45MTE4IDMuNDk5ODkgMTEuOTc0M0MyLjM1NDk1IDEyLjA0NjggMS4yOTM3NSAxMi44ODQzIDEuMzEyNSAxNC4zMTI5QzEuMzI4NzUgMTUuNTY5MSAyLjExNjIxIDE2LjU1NTMgMy43MjczNyAxNi41NTUzQzUuMDE3MzEgMTYuNTU2NiA2LjEwODUgMTUuMzU1NCA2LjAxMzUgMTQuMTM3OVpNMy42NzExMyAxNS43Mjc5QzIuNjk5OTMgMTUuNzQ1NCAyLjEzOTk2IDE1LjE3OTEgMi4xMzEyMSAxNC4zMjkyQzIuMTIzNzEgMTMuNTMzIDIuNzc0OTIgMTIuODQxOCAzLjU5NDg4IDEyLjg2NTVDNC4zODIzNCAxMi44ODggNC45NjIzMSAxMy4zMzQyIDUuMDU4NTUgMTQuMTYxN0M1LjE1NDggMTQuOTg5MSA0LjUyOTgzIDE1LjcxMjkgMy42NzExMyAxNS43Mjc5WiIgZmlsbD0iIzRENDUzQyIvPgo8cGF0aCBkPSJNMy41ODcyMyAxMy4xNjA1QzIuOTUxMDEgMTMuMTI2OCAyLjM5MzU0IDEzLjU2NTUgMi4zODYwNCAxNC4zNjkyQzIuMzc4NTQgMTUuMjIwNCAzLjE0OTc1IDE1LjUxNDEgMy41OTQ3MyAxNS41MjI5QzQuMDM5NyAxNS41MzE2IDQuNzQwOTIgMTUuMjA1NCA0Ljc2MzQyIDE0LjQxNzlDNC43ODg0MSAxMy41ODE3IDQuMzM0NjkgMTMuMTk5MyAzLjU4NzIzIDEzLjE2MDVaIiBmaWxsPSIjODU4NTg1Ii8+CjxwYXRoIGQ9Ik0xNC4xNzMxIDExLjY3MzFDMTIuOTQxOSAxMS43NTU2IDExLjg2NyAxMi44MTU1IDExLjg4MiAxNC4xNDY3QzExLjg5ODIgMTUuNTQ2NiAxMi45NzE5IDE2LjU0MDMgMTQuMzMxOSAxNi41NDAzQzE1LjY2MDUgMTYuNTQwMyAxNi43NjQyIDE1LjU2MTYgMTYuNzQxNyAxNC4wNzQyQzE2LjcxOCAxMi40NjA2IDE1LjQ2OTMgMTEuNTg1NiAxNC4xNzMxIDExLjY3MzFaTTE0LjM2NDQgMTUuNTc3OUMxMy42MDk0IDE1LjU3MDQgMTMuMDM1NyAxNC45NzI5IDEyLjk2NDQgMTQuMzIxN0MxMi44Njk0IDEzLjQ1MDUgMTMuNDE1NyAxMi44MTY4IDE0LjE4OTQgMTIuNzU0M0MxNC45OTMxIDEyLjY5MDUgMTUuNjYwNSAxMy4xNjggMTUuNzQ4IDE0LjAyNjdDMTUuODM1NSAxNC44ODU0IDE1LjE1OTMgMTUuNTg2NiAxNC4zNjQ0IDE1LjU3NzlaIiBmaWxsPSIjNEQ0NTNDIi8+CjxwYXRoIGQ9Ik0xLjg5MzY3IDEyLjI1MzFDMS45Mzk5MiAxMi4zMzQzIDIuNDU4NjQgMTIuMjkzMSAyLjg1NjEyIDEyLjQ2ODFDMy4xMTQ4NiAxMi41ODE4IDMuMzk3MzQgMTIuODgxOCAzLjM1NzM1IDEzLjI5NTVDMy4zMTczNSAxMy43MDkyIDMuMjM4NiAxNC4wOTA1IDMuMjM4NiAxNC4wOTA1TDMuOTY5ODEgMTQuMzI5MkMzLjk2OTgxIDE0LjMyOTIgNC4yNTYwNSAxMy42ODU1IDQuNDE0NzkgMTMuNTE4QzQuNTczNTMgMTMuMzUwNSA0LjkzMjI2IDEzLjAxNjggNS40NTcyMyAxMy4wODhDNS44NjA5NiAxMy4xNDMgNi4zMTU5NCAxMy41MDkzIDYuMzE1OTQgMTMuNTA5M0M2LjMxNTk0IDEzLjUwOTMgNi4wMjk3IDExLjc3NTYgNS45OTcyMSAxMS43NTk0QzUuOTY1OTYgMTEuNzQzMSA0LjM5MTA0IDExLjgyMzEgNC4zOTEwNCAxMS44MjMxQzQuMzkxMDQgMTEuODIzMSAzLjY3NDgzIDExLjUyOTQgMi45NDM2MiAxMS43MTE5QzIuMjExMTYgMTEuODk1NiAxLjg2MTE3IDEyLjE5ODEgMS44OTM2NyAxMi4yNTMxWiIgZmlsbD0iI0RCMEQyQSIvPgo8cGF0aCBkPSJNNC4yMzk5IDExLjQ5OEwzLjQ4NDk0IDEzLjc4OTJDMy40ODQ5NCAxMy43ODkyIDMuMTk2MiAxMy43OTA0IDMuMDg3NDYgMTQuMTg2N0MyLjk5OTk2IDE0LjUwNTQgMy4yMTQ5NSAxNC43MTE2IDMuNDEzNjkgMTQuNzY2NkMzLjY0ODY4IDE0LjgzMjkgMy45NTI0MSAxNC43Mjc5IDQuMDI2MTYgMTQuNDU2NkM0LjEyMTE1IDE0LjEwNjcgMy45MzExNiAxMy45NDc5IDMuOTMxMTYgMTMuOTE1NEMzLjkzMTE2IDEzLjg4MjkgNC43NzM2MiAxMS41NjE4IDQuNzczNjIgMTEuNTYxOEw0LjIzOTkgMTEuNDk4WiIgZmlsbD0iI0UxRDlEQyIvPgo8cGF0aCBkPSJNNi44NDk2MSAxMy4zMTU1TDguMjA4MjkgMTQuODQ3OUwxMC44NTE5IDE0LjgzNTRMMTIuNDcwNiAxNC4zNTI5TDEyLjI0OTMgMTMuNDg4TDEwLjkzOTQgMTIuODcwNUMxMC45Mzk0IDEyLjg3MDUgMTEuNTk0NCAxMS4wMTY5IDExLjQ1ODEgMTAuOTU1NkMxMS4zMjE5IDEwLjg5NDQgOS43OTA3IDkuODY4MTYgOS43OTA3IDkuODY4MTZMNi44ODcxMSAxMC44NDQ0TDYuODQ5NjEgMTMuMzE1NVoiIGZpbGw9IiMyRjJGMkYiLz4KPHBhdGggZD0iTTEyLjM4NjggMTEuMjI5NEMxMi4zODY4IDExLjIyOTQgMTEuOTM5MyAxMS42MzY5IDExLjY3OTMgMTEuOTk4MUMxMS40OTY5IDEyLjI1MzEgMTEuNDQ1NiAxMi41NDgxIDExLjQ0NTYgMTIuNTQ4MUwxMS44MjgxIDEyLjcyMDZDMTEuODI4MSAxMi43MjA2IDEyLjA1OTMgMTIuMTkwNiAxMi42MDA1IDExLjg3NTZDMTMuMjIxOCAxMS41MTMxIDEzLjY5NDIgMTEuNTU5NCAxMy43NjggMTEuNzE5NEMxMy44NDE3IDExLjg3OTQgMTMuNTM2NyAxMS45MzMxIDEzLjEwMDUgMTIuMzg2OEMxMi43NzkzIDEyLjcyMDYgMTIuNjA2OCAxMy4wNDE4IDEyLjYwNjggMTMuMDQxOEMxMi42MDY4IDEzLjA0MTggMTQuMDUzIDEzLjYzNDMgMTQuMDUzIDEzLjcwOTNDMTQuMDUzIDEzLjc4MyAxMy43OTMgMTQuNTI0MiAxMy43OTMgMTQuNTI0MkwxMi4xNDkzIDE0LjEyOTJDMTIuMTQ5MyAxNC4xMjkyIDExLjA4NjkgMTMuODA4IDEwLjk3NTYgMTQuMTI5MkMxMC44NjQ0IDE0LjQ1MDUgMTEuMzU4MSAxNC42MzU1IDExLjM1ODEgMTQuNjM1NUwxMC45MDA2IDE0LjkzMTdMOS40Nzk0NiAxNC4zMTQyQzkuNDc5NDYgMTQuMzE0MiA5LjM5NTcxIDEzLjkyMTggOS44MzgxOSAxMy4wNTQzQzEwLjUwNjkgMTEuNzQwNiAxMS41MDU2IDExLjA3NjkgMTEuNTA1NiAxMS4wNzY5TDEyLjM4NjggMTEuMjI5NFoiIGZpbGw9IiM1RTYyNjgiLz4KPHBhdGggZD0iTTEwLjkzNjkgMTQuNzgwNUMxMC45NDA2IDE0LjgzMyAxMC45ODA2IDE0Ljg5NTUgMTEuMTQ4MSAxNC44ODY3QzExLjMxNTYgMTQuODc4IDEyLjk5MyAxNC45MDQyIDEzLjE5NTUgMTQuODYwNUMxMy4zOTggMTQuODE2NyAxNi4xMDkxIDEzLjY0MDUgMTYuMTYyOCAxMy41MzQzQzE2LjIwMTYgMTMuNDU2OCAxNi4yMDc4IDEzLjE5NTUgMTYuMDgxNiAxMi45MjY4QzE1Ljk0MDQgMTIuNjI2OCAxNS43OTkxIDEyLjQ3NjggMTUuNjE0MSAxMi41MTE4QzE1LjQyOTEgMTIuNTQ2OCAxMS42NTE4IDE0LjM0OCAxMS4zNjkzIDE0LjQ2MTdDMTEuMDg2OSAxNC41NzY3IDEwLjkyODEgMTQuNjU2NyAxMC45MzY5IDE0Ljc4MDVaIiBmaWxsPSIjRTBFMEUwIi8+CjxwYXRoIGQ9Ik0xMi45MjE4IDE0LjI3NjdDMTIuOTUxOCAxNC4zNDggMTMuMjQ1NSAxNC4zMzggMTMuNTg2OCAxNC4xODE3QzEzLjkyOCAxNC4wMjU1IDE2LjA5OTEgMTIuOTYzIDE2LjA5OTEgMTIuOTYzQzE2LjA5OTEgMTIuOTYzIDE2LjA2MjkgMTIuODU0MyAxNS45OTkxIDEyLjc1M0MxNS45NDkxIDEyLjY3NDMgMTUuODY1NCAxMi41OTE4IDE1Ljg2NTQgMTIuNTkxOEMxNS44NjU0IDEyLjU5MTggMTMuMzk2OCAxMy44NDkyIDEzLjI1OCAxMy45MThDMTMuMDc4IDE0LjAwOCAxMi44NjU2IDE0LjE0MyAxMi45MjE4IDE0LjI3NjdaIiBmaWxsPSIjRkVGRUZFIi8+CjxwYXRoIGQ9Ik0xNS4wNjMgOS45NTE4OEwxNS45NjQyIDEwLjc0ODFDMTUuOTY0MiAxMC43NDgxIDE2LjE2NTQgMTAuNDI1NiAxNi4xNjU0IDEwLjM5NDRDMTYuMTY1NCAxMC4zNjE5IDE1LjU2MTcgOS41OTgxNCAxNS41NjE3IDkuNTk4MTRMMTUuMDYzIDkuOTUxODhaIiBmaWxsPSIjNDg0RDUxIi8+CjxwYXRoIGQ9Ik0xMi41NjY5IDExLjI5NTZDMTIuNTY2OSAxMS4yOTU2IDEzLjc5MyAxMC43Njk0IDE0LjMwOCAxMC41Mzk0QzE1LjExNDIgMTAuMTgwNyAxNS45MzkyIDkuNzE4MjEgMTYuMDg2NyA5LjYzMDcyQzE2LjM1MjkgOS40NzE5OCAxNi4zNTA0IDkuMzIzMjMgMTYuMjc2NyA5LjI2MTk5QzE2LjIwMjkgOS4yMDA3NCAxNi4wMDU0IDkuMjg2OTggMTUuOTA1NCA5LjI5OTQ4QzE1LjgwNTQgOS4zMTE5OCAxNS41MTA1IDkuMzI0NDggMTUuNTEwNSA5LjMyNDQ4TDEzLjI5OTMgOS45Mjk0NUMxMy4yOTkzIDkuOTI5NDUgMTEuODI5NCAxMC44NTU3IDExLjg2NTYgMTAuODU1N0MxMS45MDE5IDEwLjg1NTcgMTIuNTY2OSAxMS4yOTU2IDEyLjU2NjkgMTEuMjk1NloiIGZpbGw9IiNEQzBEMkEiLz4KPHBhdGggZD0iTTkuNzcwNzUgMTAuMTcxOUM5Ljc3MDc1IDEwLjE3MTkgMTAuNzM3IDExLjExMTkgMTAuODc1NyAxMS4xODE5QzExLjAxNTcgMTEuMjUxOSAxMS4yODE5IDExLjI4MzEgMTEuNTA0NCAxMS4yMzk0QzExLjcyNjkgMTEuMTk1NiAxMi45NTMxIDEwLjI2NjkgMTMuNDgwNiAxMC4xMDE5QzE0LjAwOCA5LjkzNjkzIDE0LjkxNjcgOS45MTE5MyAxNS4yMTU1IDkuNzQ1NjlDMTUuNTE0MiA5LjU3OTQ1IDE1LjUxMDQgOS4zMjQ0NiAxNS41MTA0IDkuMzI0NDZMMTIuOTE1NiA5LjUwNDQ1QzEyLjkxNTYgOS41MDQ0NSAxMi4zNjU2IDEwLjM4ODIgMTEuMzMzMiAxMC4zMzA3QzEwLjUzOTUgMTAuMjg1NyA5Ljk0MTk5IDkuNjE5NDUgOS45NDE5OSA5LjYxOTQ1TDkuNzcwNzUgMTAuMTcxOVoiIGZpbGw9IiM0NjRDNEYiLz4KPHBhdGggZD0iTTEyLjc4MzEgMTEuOTk0M0MxMy4wMTE5IDEyLjExNTYgMTMuMTIwNiAxMS44NTE4IDEzLjA2MzEgMTEuNzM0M0MxMi45NDE5IDExLjQ4NjkgMTIuNDkxOSAxMC43ODMxIDEyLjM3NjkgMTAuNzM2OUMxMi4yODE5IDEwLjY5OTQgMTEuNzY3IDExLjA3MzEgMTEuNzE1NyAxMS4xMTE5QzExLjY2NDUgMTEuMTQ5NCAxMS42ODMyIDExLjIzMDYgMTEuNzkyIDExLjMwOTRDMTEuOTMxOSAxMS40MTA2IDEyLjYwNTcgMTEuOTAwNiAxMi43ODMxIDExLjk5NDNaIiBmaWxsPSIjQzhDOEM4Ii8+CjxwYXRoIGQ9Ik01LjAwOTY2IDExLjAxNjlMMy40NTg1IDEwLjcwNTdMNC4yMjU5NiA5LjIyNjk5TDUuMDQ5NjYgOC40MDMyOUw1LjUzNzE0IDguMjI3MDVDNS41MzcxNCA4LjIyNzA1IDUuMTI1OTEgOC43NzMyNyA0LjkwNDY3IDkuMTE3QzQuNzY3MTggOS4zMzA3NCA0LjYyMjE4IDkuNjgxOTcgNC42MjIxOCA5LjY4MTk3QzQuNjIyMTggOS42ODE5NyA0Ljc5MzQzIDkuNjQ0NDcgNC45Mjk2NyA5LjYyODIyQzUuMDY1OTEgOS42MTE5NyA1LjIxNzE1IDkuNjAwNzIgNS4yMTcxNSA5LjYwMDcyQzUuMjE3MTUgOS42MDA3MiA1LjE3MjE2IDkuMzA4MjQgNS4yMzU5IDkuMTU3QzUuMjk5NjUgOS4wMDQ1IDUuNDMyMTQgOC45MDU3NiA1LjUyODM5IDguODk3MDFDNS42MjQ2MyA4Ljg4OTUxIDUuNjg4MzggOC45MzcwMSA1LjY4ODM4IDguOTM3MDFDNS42ODgzOCA4LjkzNzAxIDUuNjc0NjMgOC43MDU3NyA1LjY5NTg4IDguNjQwNzdDNS43MzU4OCA4LjUyMDc4IDYuMDcyMTEgOC4zNzk1NCA2LjQ4ODM0IDguMjI3MDVDNi45MDcwNiA4LjA3MzMgNy4zNzcwNCA3Ljg1OTU2IDcuNTAzMjggNy44ODk1NkM3LjU4MjAzIDcuOTA4MzEgNy43NjcwMiA4LjI2NTc5IDcuNjcwNzcgOC4zNjk1NEM3LjU3NDUzIDguNDczMjggNi4wODgzNiA5LjE2OTUgNi4wODgzNiA5LjE2OTVMNi4zMzU4NCA5Ljg0OTQ2TDUuMDA5NjYgMTEuMDE2OVoiIGZpbGw9IiM0NjRDNEYiLz4KPHBhdGggZD0iTTcuNzA5NzQgMTAuNzk0M0w5LjczMDg4IDEwLjcyMDZMMTAuMTI0NiAxMS4xMzE4TDguODAyMTggMTEuOTUzTDcuNTIxIDExLjY0MDVMNy40MDYwMSAxMC44NDQzTDcuNzA5NzQgMTAuNzk0M1oiIGZpbGw9IiM0NjRDNEYiLz4KPHBhdGggZD0iTTYuODQ3MDkgMTIuMDU5NEw1Ljc4MzM5IDEzLjIyMDVDNS43ODMzOSAxMy4yMjA1IDYuMTczMzcgMTMuOTEzIDYuMjgwODcgMTQuMjEwNUM2LjM5MzM2IDE0LjUyMDUgNi41NTIxIDE1LjA4MDUgNi41NTIxIDE1LjA4MDVDNi41NTIxIDE1LjA4MDUgMTAuNTg0NCAxNS4xOTU0IDEwLjc3MzEgMTUuMTcwNEMxMC45NjE5IDE1LjE0NTQgMTEuMTc1NiAxNC45MjQyIDExLjA3NjkgMTQuODM0MkMxMC45NzgxIDE0Ljc0NDIgOS40NDE5NSAxNC4xMTE4IDkuNDQxOTUgMTQuMTExOEM5LjQ0MTk1IDE0LjExMTggOC4yMzQ1MSAxNC4xOTQyIDguMDM3MDIgMTQuMTIwNUM3LjgzOTUzIDE0LjA0NjggNy4xOTU4MiAxMy4xNTkzIDcuMjMwODIgMTMuMDkxOEM3LjMxNTgxIDEyLjkyNDMgOC44ODMyMyAxMS45MDMxIDguODgzMjMgMTEuOTAzMUw4Ljc1MTk5IDExLjc2MzFMNy41NDQ1NSAxMS40OTE5TDcuNjE4MyAxMC45OTk0QzcuNjE4MyAxMC45OTk0IDguMTYwNzcgMTAuNTY0NCA4LjIyNTc2IDEwLjU2NDRDOC4yOTA3NiAxMC41NjQ0IDguOTQwNzMgMTAuNDU4MiA5LjMwMTk2IDEwLjM3NTdDOS42NjMxOSAxMC4yOTMyIDEwLjEwNjkgMTAuMTk0NSAxMC4yMTMyIDEwLjEzN0MxMC4zMTk0IDEwLjA3OTUgMTAuMzk4MSA5Ljk4MDcyIDEwLjM2OTQgOS44MjQ0OEMxMC4zNDY5IDkuNzA2OTggMTAuMDQwNyA5LjYwMzI0IDEwLjA0MDcgOS42MDMyNEw2Ljg3MDg0IDkuNDIyTDYuMjE5NjIgOS40OTA3NEM2LjIxOTYyIDkuNDkwNzQgNi4xMzIxMyA5Ljc4MzIzIDUuNjYzNCAxMC4wNzA3QzUuMTk0NjcgMTAuMzU4MiA0LjM5NzIyIDEwLjU0NDQgNC4zOTcyMiAxMC41NDQ0TDYuODQ3MDkgMTIuMDU5NFoiIGZpbGw9IiNEQzBEMkEiLz4KPHBhdGggZD0iTTMuNjIyMzEgOS4wNDQ0M0MzLjYyMjMxIDkuMDQ0NDMgNC41NTg1MSA4LjAzMzI0IDUuMDU0NzQgNy41ODk1MUM1LjQyMjIyIDcuMjYwNzggNS43MDQ3IDcuMDQ4MjkgNS44MDk3IDcuMDA4MjlDNS44NjU5NSA2Ljk4NzA0IDYuNDA5NjcgNi45NjgyOSA2LjQ2NDY2IDYuOTg0NTRDNi41MjA5MSA3LjAwMDc5IDYuNTc3MTYgNy4wODgyOSA2LjQ4ODQxIDcuMTc3MDNDNi40MDA5MiA3LjI2NDUzIDYuMTIzNDMgNy41ODIwMSA1Ljg3MzQ1IDcuODQ0NUM1Ljc1ODQ1IDcuOTY0NDkgNS41MzcyMSA4LjIyMzIzIDUuNTM3MjEgOC4yMjMyM0w1LjEyOTczIDguMzc0NDdMNC4xMDYwNCA4Ljg4NTY5TDMuNjIyMzEgOS4wNDQ0M1oiIGZpbGw9IiNBRUUzRkQiLz4KPHBhdGggZD0iTTUuNTI1OTEgOC4yMjU3MUM1LjUyNTkxIDguMjI1NzEgNC40NjIyMiA4LjQwNDQ1IDMuNzM5NzYgOC45NDY5MkMzLjAxNzMgOS40ODkzOSAyLjczNzMxIDEwLjU4MTggMi43MjEwNiAxMC42ODgxQzIuNzA0ODEgMTAuNzk0MyAyLjU2NzMyIDExLjE0ODEgMi44Mzg1NiAxMS4zMThDMi45Njk4IDExLjQwMDUgMy4yMzEwNCAxMS4zNjE4IDMuNTA5NzcgMTEuNDI4QzMuNzQxMDEgMTEuNDgxOCA0LjI2NDczIDExLjY1OCA0LjY2NzIxIDEyLjAxOTNDNS4wNjk2OSAxMi4zODA1IDUuNzg0NjUgMTMuMjI2NyA1Ljc4NDY1IDEzLjIyNjdMOC43NTE5OSAxMS43NjQzQzguNzUxOTkgMTEuNzY0MyA3Ljg5NTc5IDEwLjc2MzEgNS45MTU4OSAxMC42MzE4QzMuOTM2IDEwLjUwMDYgMy43MzEwMSAxMC41NDkzIDMuNzMxMDEgMTAuNTQ5M0MzLjczMTAxIDEwLjU0OTMgMy45Nzk3NSA5LjgzNDM3IDQuMzMwOTggOS4zNjY5QzQuNzk5NyA4Ljc0MTkzIDUuNTI1OTEgOC4yMjU3MSA1LjUyNTkxIDguMjI1NzFaIiBmaWxsPSIjRkUyQTIyIi8+CjxwYXRoIGQ9Ik04LjM3NzE0IDEzLjIxMThDOC4zNzcxNCAxMy4yMTE4IDguMTg1OSAxMi42NzY4IDguNjM1ODcgMTIuMzY4MUM4Ljk3NTg1IDEyLjEzNDMgOS40MDcwOCAxMi4xNjU2IDkuNjk5NTcgMTIuNTU0M0M5Ljk5MjA1IDEyLjk0MyA5LjczMDgxIDEzLjM5OTMgOS41NzU4MiAxMy41MjhDOS4wNjk2IDEzLjk1MDUgOC42MDgzNyAxMy41NTA1IDguNjA4MzcgMTMuNTUwNUM4LjYwODM3IDEzLjU1MDUgOC42MzgzNyAxMy43NzA1IDguNDM5NjMgMTMuOTIxN0M4LjI5ODM5IDE0LjAyOTIgNy45NDQ2NiAxNC4wNjA1IDcuNzk4NDIgMTMuNzkxN0M3LjY0MDkzIDEzLjUwMTggNy44MDIxNyAxMy4yODY4IDcuOTY3MTYgMTMuMTk1NUM4LjE3OTY1IDEzLjA3NjggOC4zNzcxNCAxMy4yMTE4IDguMzc3MTQgMTMuMjExOFoiIGZpbGw9IiM0NjRDNEYiLz4KPHBhdGggZD0iTTMuMDQ3MjggOS44MTE5NEMzLjA0NzI4IDkuODExOTQgMy43MjEgOS44MDgxOSAzLjcyNiA5LjgzNTY5QzMuNzMxIDkuODYzMTkgMy40Nzg1MSAxMC41MDE5IDMuNDc4NTEgMTAuNTAxOUMzLjQ3ODUxIDEwLjUwMTkgMi43MDczIDEwLjkwMTkgMi42ODM1NSAxMC44OTY5QzIuNjU5ODEgMTAuODkxOSAyLjY3ODU1IDEwLjU1MzIgMi43Nzg1NSAxMC4yNzgyQzIuODc4NTQgMTAuMDAzMiAzLjA0NzI4IDkuODExOTQgMy4wNDcyOCA5LjgxMTk0WiIgZmlsbD0iI0Q5RTNERiIvPgo8cGF0aCBkPSJNNi4yMTg1MSA5LjQ5MzE2QzYuMjE4NTEgOS40OTMxNiA2LjgwNTk3IDkuOTkwNjQgNy44NjU5MiAxMC4wODA2QzguOTI1ODYgMTAuMTcwNiAxMC4wNDIxIDkuNjA0NDEgMTAuMDQyMSA5LjYwNDQxQzEwLjA0MjEgOS42MDQ0MSA4LjI1NDY1IDguNzc2OTUgOC4wMzg0MSA4Ljc5MTk1QzcuODA1OTIgOC44MDY5NSA2LjIyNzI2IDkuNDQzMTYgNi4yMTg1MSA5LjQ5MzE2WiIgZmlsbD0iI0ZFMkEyMiIvPgo8cGF0aCBkPSJNMy4zMjA5OSA4LjY0MkMzLjM1MjIzIDguNzU0NDkgMy44MTk3MSA5LjEwNjk3IDQuMTIzNDQgOS4wNzk0N0M0LjQwMjE4IDkuMDU0NDggNC4zNTQ2OCA4LjUzNTc1IDQuMzUyMTggOC40MDgyNkM0LjM0ODQzIDguMTUwNzcgNC4zMjU5MyA3LjkzMzI4IDQuMzEwOTMgNy43OTgyOUM0LjI5NDY4IDcuNjUwOCA0LjE3MjE5IDcuNjM0NTUgNC4wODU5NSA3LjcwNDU1QzMuOTk5NyA3Ljc3MzI5IDMuMjg3MjQgOC41MjMyNSAzLjMyMDk5IDguNjQyWiIgZmlsbD0iIzQ2NEM0RiIvPgo8L3N2Zz4K', + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjIzOTUgMTMuOTMwNUMxMy4yMzk1IDEzLjkzMDUgMTMuMjI4MyAxNS4yNjQyIDEzLjIyODMgMTUuNTI5MkMxMy4yMjgzIDE1Ljc5NDEgMTMuMzc4MyAxNS45MzE2IDEzLjU1MDggMTUuOTIwNEMxMy43MjMyIDE1LjkwOTEgMTQuNTUyIDE1LjkyMDQgMTQuNzM1NyAxNS45MjA0QzE0LjkxOTQgMTUuOTIwNCAxNS4xMjY5IDE1Ljc0NzkgMTUuMTI2OSAxNS40OTU0QzE1LjEyNjkgMTUuMjQyOSAxNS4xNjE5IDEyLjk4OCAxNS4xNjE5IDEyLjk4OEwxMy4yMzk1IDEzLjkzMDVaIiBmaWxsPSIjNEU0MzNEIi8+CjxwYXRoIGQ9Ik00LjQyOTk3IDEzLjE0OTJMMi42MDEzMiAxMi44OTY3QzIuNjAxMzIgMTIuODk2NyAyLjU5MDA3IDE1LjIzMTYgMi41Nzg4MiAxNS40NTAzQzIuNTY3NTcgMTUuNjY5MSAyLjY5MzgxIDE1LjgyNzggMi44Nzc1NiAxNS44NDE2QzMuMjAwMDQgMTUuODY0MSAzLjgwODc2IDE1Ljg2NDEgNC4wMjc0OSAxNS44NTI4QzQuMjQ2MjMgMTUuODQxNiA0LjM5NDk3IDE1LjY4MDMgNC40MDc0NyAxNS40NzI4QzQuNDE5OTcgMTUuMjY1NCA0LjQwNzQ3IDE0LjAyNDIgNC40MDc0NyAxNC4wMjQyTDQuNDI5OTcgMTMuMTQ5MloiIGZpbGw9IiM0RTQzM0QiLz4KPHBhdGggZD0iTTE0LjA2MDYgOC4wODk2M0wxNS4yMDkzIDguMDM4MzhDMTUuMjA5MyA4LjAzODM4IDE1LjYzOCA4LjcxOTU5IDE1LjU4NDMgMTEuMDI0NUMxNS41MzMgMTMuMjEzMSAxNC45NjgxIDE0LjA2ODEgMTQuNDY4MSAxNC4xODkzQzEzLjk2ODEgMTQuMzEwNSAxMi4wODA3IDE0LjQ5NjggOS4wOTA4OCAxNC41MTQzQzYuMTAxMDMgMTQuNTMxOCAzLjg3OTkgMTQuMzYwNSAzLjQ1MjQyIDE0LjI1ODFDMy4wMjQ5NSAxNC4xNTU2IDIuNTYyNDcgMTMuNTIzMSAyLjUxMjQ3IDEzLjA2MTlDMi40MzYyMyAxMi4zNTQ0IDIuNDMxMjMgMTEuNDAzMiAyLjQ2NDk3IDEwLjA1MzNDMi40OTg3MiA4LjcwMzM0IDIuNzkyNDYgOC4wMjU4OCAyLjc5MjQ2IDguMDI1ODhMMTQuMDYwNiA4LjA4OTYzWiIgZmlsbD0iIzQ3NEM0RiIvPgo8cGF0aCBkPSJNNC4zNTExNSAxMS44OTgxQzMuNzAzNjkgMTEuNzkwNiAyLjc2Mzc0IDExLjM4MzEgMi42OTI0OSAxMS42ODMxQzIuNjIxMjUgMTEuOTgzMSAyLjkwNzQ4IDEyLjM0MDYgMy41OTM3IDEyLjU2OTNDNC4yNzk5MSAxMi43OTgxIDUuODUyMzMgMTIuOTY5MyA5LjE1NDY1IDEyLjg5ODFDMTIuNDU3IDEyLjgyNjggMTMuNTcxOSAxMi43NDA2IDE0LjIyOTQgMTIuNTU1NkMxNC44ODY4IDEyLjM2OTMgMTUuNjQ0MyAxMS44Njk0IDE1LjQwMTggMTEuNTgzMUMxNS4xNTkzIDExLjI5NjkgMTQuNTE1NiAxMS43MTE5IDEzLjkyOTQgMTEuODI1NkMxMy4zNDMyIDExLjkzOTQgMTEuMTQwOCAxMi4wMDA2IDkuMTEyMTUgMTIuMDQwNkM2LjkxMTAyIDEyLjA4MzEgNS4wMzczNyAxMi4wMTE4IDQuMzUxMTUgMTEuODk4MVoiIGZpbGw9IiM4NTg1ODUiLz4KPHBhdGggZD0iTTUuNTcxMTQgMTAuMDIwNkM1LjM1NzQgOS45NjQzNSAyLjcwNTA0IDkuOTAzMSAyLjU4ODc5IDEwLjAyODFDMi41Mzc1NSAxMC4wODMxIDIuNTUwMDUgMTAuODc5MyAyLjU2MDA0IDExLjAyNDNDMi41NzAwNCAxMS4xNjkzIDIuNjgxMjkgMTEuMjE5MyAyLjg3Mzc4IDExLjIyOTNDMy4wNjc1MiAxMS4yMzkzIDUuNTQyMzkgMTEuMjU4IDUuNzA2MTMgMTEuMjM5M0M1Ljg2OTg3IDExLjIyMDUgNS45MDg2MiAxMS4wNTU1IDUuODc5ODcgMTAuOTEwNUM1Ljg1MTEyIDEwLjc2NDMgNS43NTQ4OCAxMC4wNjkzIDUuNTcxMTQgMTAuMDIwNloiIGZpbGw9IiNGRUE4MjYiLz4KPHBhdGggZD0iTTE1LjM1NDQgOS44OTY5OUMxNS4yMzMxIDkuODI1NzQgMTIuNTQyIDkuOTMzMjQgMTIuNDU1OCAxMC4wMTA3QzEyLjM2ODMgMTAuMDg4MiAxMS45MDIgMTEuMDUwNyAxMi4xMzcgMTEuMTQxOUMxMi4zNTk1IDExLjIyOTQgMTUuMjY4MSAxMS4xODMyIDE1LjM2NDQgMTEuMTM0NEMxNS41MTQzIDExLjA1OTQgMTUuNDY5NCA5Ljk2NDQ5IDE1LjM1NDQgOS44OTY5OVoiIGZpbGw9IiNGRUE4MjYiLz4KPHBhdGggZD0iTTYuOTUzNDUgMTEuNDQxOUM2LjY5MjIxIDExLjQ1NDQgNi4zMjQ3MyAxMS4zMTU2IDYuMTY5NzQgMTAuOTEwN0M2LjAxNDc1IDEwLjUwNDQgNS45OTYgMTAuMjYzMiA2LjA0MzUgMTAuMDUwN0M2LjA5MjI0IDkuODM4MjIgNi4yMzcyNCA5Ljc2MDcyIDYuNTc0NzIgOS43MzE5OEM2LjkxMzQ1IDkuNzAzMjMgNy41NzA5MiA5LjY1NDQ4IDguOTgyMDkgOS42NDQ0OEMxMC4zOTQ1IDkuNjM0NDggMTEuMzcwNyA5LjY2MzIzIDExLjYyMiA5LjY5MzIzQzExLjg3MzIgOS43MjE5OCAxMi4wMTgyIDkuODA5NDcgMTEuOTc5NCAxMC4wNzA3QzExLjk0MDcgMTAuMzMxOSAxMS44MDU3IDEwLjg1NDQgMTEuNjk5NCAxMS4xMDU3QzExLjYyMiAxMS4yODk0IDExLjM4MDcgMTEuNDI0NCAxMS4wNjA3IDExLjQzNDRDMTAuNzQzMiAxMS40NDE5IDcuMTU3MTkgMTEuNDMxOSA2Ljk1MzQ1IDExLjQ0MTlaIiBmaWxsPSIjMkYyRjJGIi8+CjxwYXRoIGQ9Ik0xNC42NjggNy40MTk2NUwxNS4wNDQyIDcuNDI5NjVDMTUuMDQ0MiA3LjQyOTY1IDE1LjA1MjkgNi45NzcxNyAxNS4xMDQyIDYuODM3MThDMTUuMTU1NCA2LjY5NzE5IDE1LjI1OTIgNi42MjIxOSAxNS40OTQyIDYuNjIyMTlDMTUuNzI5MiA2LjYyMjE5IDE2LjE5MDQgNi42Mzk2OSAxNi4zNDU0IDYuNjQ3MTlDMTYuNTAwNCA2LjY1NDY5IDE2LjgwMTYgNi45MTk2OCAxNi44MTY2IDcuMzE3MTZDMTYuODMxNiA3LjcxNDY0IDE2LjU1OTEgNy45NjQ2MiAxNi4yODY2IDcuOTk0NjJDMTYuMDE0MSA4LjAyNDYyIDE0Ljk5MTcgNy45OTQ2MiAxNC45OTE3IDcuOTk0NjJMMTQuNjY4IDcuNDE5NjVaIiBmaWxsPSIjNDc0QzRGIi8+CjxwYXRoIGQ9Ik0zLjQ1MTA2IDcuMzU3MTRMMy4wMzM1OCA3LjM4MzM5QzMuMDMzNTggNy4zODMzOSAzLjA0NjA4IDYuOTc0NjYgMy4wMTczMyA2LjgyNzE3QzIuOTg4NTggNi42Nzk2OCAyLjgxMTA5IDYuNTkyMTkgMi42Nzg2IDYuNTg0NjlDMi41NDYxMSA2LjU3NzE5IDEuOTI3MzkgNi41NzcxOSAxLjc1MTE1IDYuNTk5NjhDMS41NzQ5MSA2LjYyMjE4IDEuMjcyNDIgNi43OTg0MiAxLjI3OTkyIDcuMjQ3MTVDMS4yODc0MiA3LjY5NTg4IDEuNTc0OTEgNy45NDU4NiAxLjgxNzM5IDcuOTM5NjFDMi4wNTk4OCA3LjkzMzM2IDIuNjQ3MzUgNy45NTIxMSAyLjgyNjA5IDcuOTQ3MTFDMy4wNTQ4MyA3LjkzOTYxIDMuNTU0OCA3Ljk2MjExIDMuNTU0OCA3Ljk2MjExTDMuNDUxMDYgNy4zNTcxNFoiIGZpbGw9IiM0NzRDNEYiLz4KPHBhdGggZD0iTTcuOTU5NzIgMy4wOTgzOUw3Ljk3ODQ3IDMuNjg3MTFIMTAuMzU3MUwxMC4zNzU4IDMuMTE3MTRMNy45NTk3MiAzLjA5ODM5WiIgZmlsbD0iIzQ3NEM0RiIvPgo8cGF0aCBkPSJNMTEuNjIzMyAzLjA3ODU4QzExLjYyMzMgMy4yMjM1NyAxMS42MjMzIDMuODQyMjkgMTEuNjIzMyAzLjg0MjI5TDEyLjQwNyAzLjg2MTA0TDEyLjM1ODMgMy4wNDg1OEwxMS42MjMzIDMuMDc4NThaIiBmaWxsPSIjNDc0QzRGIi8+CjxwYXRoIGQ9Ik01LjczNDg2IDMuODMyMjNDNS43NTM2MSAzLjcxNTk4IDUuNzYzNjEgMi44OTQ3OCA1Ljc2MzYxIDIuODk0NzhMNi42MDQ4MiAzLjAxMTAyTDYuNTc2MDcgMy43NjQ3M0w1LjczNDg2IDMuODMyMjNaIiBmaWxsPSIjNDc0QzRGIi8+CjxwYXRoIGQ9Ik0yLjc4ODggOC4wMjU4MkMyLjgxMDA1IDcuOTg1ODIgMy4wNjc1NCA3Ljg5NDU4IDMuMjIyNTMgNy42MjIwOUMzLjMzMTI4IDcuNDMwODUgMy42NDc1MSA2LjQwMzQxIDQuMDYzNzQgNS41ODIyQzQuNDc5OTYgNC43NjA5OSA0LjY1MzcxIDQuNDAyMjYgNC43Njk5NSA0LjIxODUyQzQuODg2MTkgNC4wMzQ3OCA1LjA1OTkzIDMuODg5NzkgNS4zNTk5MiAzLjgwMjI5QzUuNjU5OSAzLjcxNDggNi43MDQ4NSAzLjQ3ODU2IDkuMjE4NDYgMy41MjczMUMxMS43MzIxIDMuNTc2MDYgMTIuMzY5NSAzLjY1MTA1IDEyLjYxNDUgMy43Mjk4QzEyLjgxMiAzLjc5MjI5IDEzLjE4MiAzLjk4NjAzIDEzLjQyMzIgNC40MzEwMUMxMy42NjQ1IDQuODc1OTkgMTQuNjg4MiA3LjE1OTYyIDE0Ljc4NTcgNy4zNjIxMUMxNC44ODE5IDcuNTY0NiAxNS4yMTA2IDguMDM1ODIgMTUuMjEwNiA4LjAzNTgyQzE1LjIxMDYgOC4wMzU4MiAxNC4zNDE5IDguOTM5NTIgMTIuMzEyMSA5LjE3NDUxQzEwLjQ4NzEgOS4zODU3NSA3LjgzNzI5IDkuNTc0NDkgNS4xMDExOCA5LjEwMjAyQzIuOTUzOCA4LjczNDUzIDIuNzY3NTYgOC4wNjU4MiAyLjc4ODggOC4wMjU4MloiIGZpbGw9IiNFM0U0REUiLz4KPHBhdGggZD0iTTUuMDE5OTEgNC41NTcyNUM0Ljc5NzQzIDQuNzc5NzQgNC4yOTYyIDUuODc5NjggNC4wNDc0NyA2LjUxMjE1QzMuNzk4NzMgNy4xNDQ2MSAzLjY1MjQ5IDcuNjY0NTggMy45OTEyMiA3LjY3NTgzQzQuMzI5OTUgNy42ODcwOCA3LjU1MTAzIDcuNjQyMDkgOC45NzU5NiA3LjYzMDg0QzEwLjQwMDkgNy42MTk1OSAxMy44MDIgNy42OTgzMyAxNC4wNTA3IDcuNjk4MzNDMTQuMjk5NCA3LjY5ODMzIDE0LjQ0NTcgNy40ODMzNCAxNC4zMzMyIDcuMjEyMTFDMTQuMjIwNyA2Ljk0MDg3IDEzLjM4MzIgNS4wODcyMiAxMy4xOTIgNC43ODIyNEMxMi45OTk1IDQuNDc3MjUgMTIuNzI4MyA0LjM3NDc2IDEyLjI1MzMgNC4zNTIyNkMxMS43NzgzIDQuMzI5NzYgMTAuMDAzNCA0LjIyODUyIDguNzgzNDcgNC4yMjg1MkM3LjU2MzUzIDQuMjI4NTIgNS4yMjM2NSA0LjM1NDc2IDUuMDE5OTEgNC41NTcyNVoiIGZpbGw9IiMyRDJFMzUiLz4KPHBhdGggZD0iTTQuMjYzNDYgNy4xNTA2OUM0LjMxMjIxIDcuMjI0NDQgNy4zNzA4IDcuMDkwNyA5LjIxMzIgNy4xMjQ0NEMxMS4wNTU2IDcuMTU4MTkgMTMuNTE5MiA3LjIxNDQ0IDEzLjY0NDIgNy4yMDMxOUMxMy43NjggNy4xOTE5NCAxMy43ODA1IDcuMDEwNyAxMy43NjggNi45NTQ0NUMxMy43NTY3IDYuODk4MjEgMTMuNTQxNyA2LjQ1Njk4IDEzLjU0MTcgNi40NTY5OEMxMy41NDE3IDYuNDU2OTggMTIuNDIzIDUuMTkwOCAxMi4zMzE4IDUuMTkwOEMxMi4yNDE4IDUuMTkwOCA5LjA3Njk2IDUuMzk0NTQgOS4wNzY5NiA1LjM5NDU0TDUuNTE3MTUgNS4yOTMyOUw0LjQ0MzQ1IDYuNjE4MjJDNC40NDM0NSA2LjYxODIyIDQuMjQwOTYgNy4xMTY5NCA0LjI2MzQ2IDcuMTUwNjlaIiBmaWxsPSIjOTZDOEVEIi8+CjxwYXRoIGQ9Ik00LjQ0MzYgNi42MTg0NEM0LjQ0MzYgNi42MTg0NCA0Ljk0MjMzIDUuNDI0NzUgNS4xNDM1NyA1LjEyMjI3QzUuMjIyMzEgNS4wMDQ3OCA1LjI3OTgxIDQuODk2MDMgNS41NTEwNSA0Ljg2MjI4QzUuODIyMjggNC44Mjg1MyA3LjIyMzQ2IDQuNjI0OCA5LjA5OTYxIDQuNjU4NTRDMTAuOTc1OCA0LjY5MjI5IDEyLjA2MDcgNC43NjcyOSAxMi4yODY5IDQuNzgyMjlDMTIuNTU0NCA0Ljc5OTc5IDEyLjczOTQgNC44OTQ3OCAxMi44NDA3IDUuMDY0NzdDMTIuOTQxOSA1LjIzNDc2IDEzLjY1NDQgNi42OTIxOSAxMy42NTQ0IDYuNjkyMTlMMTIuNTA0NCA2LjU4NzE5QzEyLjUwNDQgNi41ODcxOSAxMi40OTY5IDYuMDU3MjIgMTIuNDc5NCA1Ljg3NDczQzEyLjQ2NDQgNS43MTcyNCAxMi4zOTA3IDUuNTg3MjQgMTIuMTQ5NCA1LjU3MzVDMTEuOTM0NSA1LjU2MjI1IDExLjQ1OTUgNS41NTEgMTEuMzAyIDUuNTYyMjVDMTEuMTQzMiA1LjU3MzUgMTEuMDM1OCA1LjcwNDc0IDExLjAwNDUgNS45Mjk3M0MxMC45NzMzIDYuMTU1OTYgMTAuOTQwOCA2LjU1OTY5IDEwLjk0MDggNi41NTk2OUw3LjM3OTcgNi41ODA5NEM3LjM3OTcgNi41ODA5NCA3LjM3OTcgNi4wMjM0NyA3LjM0NzIgNS44MjIyM0M3LjMyMzQ1IDUuNjc3MjQgNy4yMzIyMSA1LjU4MjI0IDcuMDQ1OTcgNS41ODM0OUM2LjgyOTczIDUuNTg0NzQgNi40MTk3NSA1LjU2MjI1IDYuMTI2MDEgNS41OTU5OUM1Ljk4OTc3IDUuNjEyMjQgNS44ODg1MyA1Ljc3NzIzIDUuODc3MjggNi4wMTQ3MkM1Ljg2ODUzIDYuMTg1OTYgNS44NTQ3OCA2LjU2ODQ0IDUuODU0NzggNi41Njg0NEw0LjQ0MzYgNi42MTg0NFoiIGZpbGw9IiNBRkUzRkIiLz4KPHBhdGggZD0iTTguMDU2MDcgMS45ODYyMUgxMC4zMjg1TDEwLjc0MzQgMi43MzExN0wxMC4zMDg1IDMuMjcyMzlIOC4wNTYwN0w3LjYyMTA5IDIuNTk0OTJMOC4wNTYwNyAxLjk4NjIxWiIgZmlsbD0iI0NFQ0VDRiIvPgo8cGF0aCBkPSJNMTAuMzI4MyAxLjk4NjIxQzEwLjMyODMgMS45ODYyMSAxMi4yODA3IDIuMDE0OTUgMTIuNDE2OSAyLjAzNDk1QzEyLjU1MzIgMi4wNTQ5NSAxMi42ODgyIDIuMjE4NjkgMTIuNzA2OSAyLjQxMjQzQzEyLjcyNTcgMi42MDYxNyAxMi42ODgyIDMuMDc5OSAxMi42ODgyIDMuMTY2MTRDMTIuNjg4MiAzLjI1MjM5IDEyLjU3NjkgMy4zMDQ4OSAxMi4zOTQ0IDMuMzA0ODlDMTIuMjEwNyAzLjMwNDg5IDEwLjMxMDggMy4yNzIzOSAxMC4zMTA4IDMuMjcyMzlMMTAuMzI4MyAxLjk4NjIxWiIgZmlsbD0iI0REMEMyNiIvPgo8cGF0aCBkPSJNNS40MjYwMyAzLjAyMTE1QzUuNDI2MDMgMy4xNDc0IDUuNDM2MDMgMy4yNDM2NCA1LjU2MTAyIDMuMjUzNjRDNS42ODcyNiAzLjI2MzY0IDguMDU1ODkgMy4yNzIzOSA4LjA1NTg5IDMuMjcyMzlWMS45ODYyMUM4LjA1NTg5IDEuOTg2MjEgNS44OTk3NSAxLjk4NjIxIDUuNzQ0NzYgMS45ODYyMUM1LjU4OTc3IDEuOTg2MjEgNS40MjYwMyAyLjE2OTk1IDUuNDI2MDMgMi4zMTQ5NEM1LjQyNjAzIDIuNTE3NDMgNS40MjYwMyAzLjAyMTE1IDUuNDI2MDMgMy4wMjExNVoiIGZpbGw9IiMwRjY0QzciLz4KPC9zdmc+Cg==', +]; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.styles.scss b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.styles.scss index 31b92a4b3e..addfa73bf8 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.styles.scss +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.styles.scss @@ -6,3 +6,485 @@ margin-bottom: 1rem; flex-direction: column; } + +.variable-item-container { + display: flex; + flex-direction: column; + border-radius: 3px; + border: 1px solid var(--bg-slate-500); + + .all-variables { + display: flex; + padding: 10px 16px; + align-items: center; + gap: 8px; + border-bottom: 1px solid var(--bg-slate-500); + .all-variables-btn { + display: flex; + align-items: center; + height: 24px; + padding: 0px; + color: #c0c1c3; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + } + + .all-variables-btn:hover { + background-color: unset !important; + } + } + + .variable-item-content { + padding: 12px 16px 20px 16px; + display: flex; + flex-direction: column; + gap: 20px; + + .variable-name-section { + flex-direction: column; + gap: 8px; + + .typography-variables { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + } + + .name-input { + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + padding: 6px 6px 6px 8px; + } + } + .variable-description-section { + flex-direction: column; + gap: 8px; + + .typography-variables { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + } + + .description-input { + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + padding: 6px 6px 6px 8px; + } + } + + .variable-type-section { + justify-content: space-between; + margin-bottom: 0px; + align-items: center; + + .typography-variables { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + } + + .variable-type-btn-group { + display: flex; + width: 342px; + height: 32px; + flex-shrink: 0; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-400); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + + .ant-btn { + width: 114px; + height: 32px; + flex-shrink: 0; + border-radius: 2px 0px 0px 2px; + } + + .variable-type-btn { + display: flex; + align-items: center; + justify-content: center; + } + + .variable-type-btn + .variable-type-btn { + border-left: 1px solid var(--bg-slate-400); + } + .selected { + background: var(--bg-slate-400); + } + } + } + + .sort-values-section { + justify-content: space-between; + margin-bottom: 0; + + .typography-variables { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .typography-sort { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + letter-spacing: -0.06px; + } + + .sort-input { + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + + .ant-select-selector { + width: 192px; + height: 32px; + padding: 6px 6px 6px 8px; + background: var(--bg-ink-300); + } + } + } + + .multiple-values-section { + justify-content: space-between; + margin-bottom: 0; + + .typography-variables { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + width: 339px; + } + } + + .all-option-section { + justify-content: space-between; + margin-bottom: 0; + + .typography-variables { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + width: 339px; + } + } + + .variable-textbox-section { + justify-content: space-between; + margin-bottom: 0; + align-items: center; + + .typography-variables { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + } + + .default-input { + display: flex; + height: 32px; + padding: 6px 6px 6px 8px; + align-items: flex-start; + gap: 4px; + flex: 1 0 0; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + width: 342px; + } + } + + .variable-custom-section { + margin-bottom: 0px; + .custom-collapse { + width: 100%; + border-radius: 3px 3px 0px 0px; + border: 1px solid var(--bg-slate-500); + + .ant-collapse-item { + border-bottom: none; + } + + .ant-collapse-header { + height: 38px; + border-radius: 3px 3px 0px 0px; + background: var(--bg-ink-300); + align-items: center; + padding: 12px; + gap: 8px; + + .ant-collapse-expand-icon { + padding-inline-end: 0px; + } + + .ant-collapse-header-text { + color: var(--bg-robin-400); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + display: flex; + padding: 1px 2px; + align-items: center; + gap: 10px; + border-radius: 2px; + background: rgba(113, 144, 249, 0.08); + } + } + + .ant-collapse-content { + border-top: none; + + .ant-collapse-content-box { + padding: 0px; + } + + .comma-input { + height: 109px; + border: none; + } + } + } + } + + .variables-preview-section { + display: flex; + flex-direction: column; + margin-bottom: 0px; + border-radius: 0px 0px 3px 3px; + border: 1px solid var(--bg-slate-500); + height: 108px; + margin-top: -20px; + border-collapse: collapse; + gap: 5px; + flex-flow: column; + + .typography-variables { + color: var(--bg-robin-400); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + display: inline-flex; + padding: 1px 2px; + align-items: center; + gap: 10px; + border-radius: 0px 0px 2px 0px; + background: rgba(113, 144, 249, 0.08); + } + + .preview-values { + padding: 4.5px 11px; + display: flex; + overflow-y: auto; + flex-flow: wrap; + gap: 8px; + + .ant-tag { + height: 30px; + color: var(--bg-vanilla-100); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + display: inline-flex; + letter-spacing: -0.07px; + align-items: center; + border-radius: 2px; + border: 1px solid var(--bg-slate-300); + margin-inline-end: 0px; + } + } + } + } +} + +.variable-item-footer { + margin-top: 12px; + display: flex; + flex-direction: row-reverse; + + .footer-btn-discard { + display: flex; + align-items: center; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-slate-500); + height: 34px; + padding: 4px 8px 4px 10px; + box-shadow: none; + } + + .footer-btn-save { + display: flex; + align-items: center; + border-radius: 2px; + border: 1px solid var(--bg-robin-500); + background: var(--bg-robin-500); + width: 123px; + height: 34px; + padding: 4px 8px 4px 10px; + box-shadow: none; + } +} + +.lightMode { + .variable-item-container { + border: 1px solid var(--bg-vanilla-300); + + .all-variables { + border-bottom: 1px solid var(--bg-vanilla-300); + .all-variables-btn { + color: var(--bg-ink-300); + } + } + + .variable-item-content { + .variable-name-section { + .typography-variables { + color: var(--bg-ink-400); + } + + .name-input { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + } + } + .variable-description-section { + .typography-variables { + color: var(--bg-ink-400); + } + + .description-input { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + } + } + + .variable-type-section { + .typography-variables { + color: var(--bg-slate-400); + } + + .variable-type-btn-group { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + + .variable-type-btn + .variable-type-btn { + border-left: 1px solid var(--bg-vanilla-200); + } + .selected { + background: var(--bg-vanilla-200); + } + } + } + + .sort-values-section { + .typography-variables { + color: var(--bg-ink-300); + } + + .typography-sort { + color: var(--bg-ink-400); + } + + .sort-input { + border: 1px solid var(--bg-vanilla-300); + + .ant-select-selector { + background: var(--bg-vanilla-300); + border: var(--bg-vanilla-300); + } + } + } + + .multiple-values-section { + .typography-variables { + color: var(--bg-ink-400); + } + } + + .all-option-section { + .typography-variables { + color: var(--bg-ink-400); + } + } + + .variable-textbox-section { + .typography-variables { + color: var(--bg-ink-400); + } + + .default-input { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + color: var(--bg-ink-300); + } + } + + .variable-custom-section { + .custom-collapse { + border: 1px solid var(--bg-vanilla-300); + + .ant-collapse-header { + background: var(--bg-vanilla-300); + } + } + } + + .variables-preview-section { + border: 1px solid var(--bg-vanilla-300); + + .preview-values { + .ant-tag { + color: var(--bg-slate-300); + border: 1px solid var(--bg-vanilla-300); + } + } + } + } + } + + .variable-item-footer { + .footer-btn-discard { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-200); + } + } +} diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx index a9b0aea09f..37e8da6bcf 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx @@ -2,20 +2,28 @@ import './VariableItem.styles.scss'; import { orange } from '@ant-design/colors'; -import { Button, Divider, Input, Select, Switch, Tag, Typography } from 'antd'; +import { Button, Collapse, Input, Select, Switch, Tag, Typography } from 'antd'; import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery'; +import cx from 'classnames'; import Editor from 'components/Editor'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; import sortValues from 'lib/dashbaordVariables/sortVariableValues'; import { map } from 'lodash-es'; +import { + ArrowLeft, + Check, + ClipboardType, + DatabaseZap, + LayoutList, + X, +} from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { useQuery } from 'react-query'; import { IDashboardVariable, TSortVariableValuesType, TVariableQueryType, - VariableQueryTypeArr, VariableSortTypeArr, } from 'types/api/dashboard/getAll'; import { v4 as generateUUID } from 'uuid'; @@ -79,7 +87,6 @@ function VariableItem({ const [errorPreview, setErrorPreview] = useState(null); useEffect(() => { - setPreviewValues([]); if (queryType === 'CUSTOM') { setPreviewValues( sortValues( @@ -88,6 +95,9 @@ function VariableItem({ ) as never, ); } + if (queryType === 'QUERY') { + setPreviewValues((prev) => sortValues(prev, variableSortType) as never); + } }, [ queryType, variableCustomValue, @@ -121,13 +131,16 @@ function VariableItem({ // Fetches the preview values for the SQL variable query const handleQueryResult = (response: any): void => { - if (response?.payload?.variableValues) + if (response?.payload?.variableValues) { setPreviewValues( sortValues( response.payload?.variableValues || [], variableSortType, ) as never, ); + } else { + setPreviewValues([]); + } }; const { isFetching: previewLoading, refetch: runQuery } = useQuery( @@ -169,219 +182,288 @@ function VariableItem({ }, []); return ( -
-
- - - Name - -
- { - setVariableName(e.target.value); - setErrorName( - !validateName(e.target.value) && e.target.value !== variableData.name, - ); - }} - /> -
- - {errorName ? 'Variable name already exists' : ''} - -
-
-
- - - Description - - - setVariableDescription(e.target.value)} - /> - - - - Type - - - - - - Options - - {queryType === 'QUERY' && ( -
+ All variables + +
+
+ - Query + Name - -
- setVariableQueryValue(e)} - height="240px" - options={{ - fontSize: 13, - wordWrap: 'on', - lineNumbers: 'off', - glyphMargin: false, - folding: false, - lineDecorationsWidth: 0, - lineNumbersMinChars: 0, - minimap: { - enabled: false, - }, +
+ { + setVariableName(e.target.value); + setErrorName( + !validateName(e.target.value) && e.target.value !== variableData.name, + ); }} /> +
+ + {errorName ? 'Variable name already exists' : ''} + +
+
+ + + + Description + + + setVariableDescription(e.target.value)} + /> + + + + Variable Type + + +
+ +
-
- )} - {queryType === 'CUSTOM' && ( - - - Values separated by comma - - { - setVariableCustomValue(e.target.value); - setPreviewValues( - sortValues( - commaValuesParser(e.target.value), - variableSortType, - ) as never, - ); - }} - /> - )} - {queryType === 'TEXTBOX' && ( - - - Default Value - - { - setVariableTextboxValue(e.target.value); - }} - placeholder="Default value if any" - style={{ width: 400 }} - /> - - )} - {(queryType === 'QUERY' || queryType === 'CUSTOM') && ( - <> - + {queryType === 'QUERY' && ( +
- Preview of Values - -
- {errorPreview ? ( - {errorPreview} - ) : ( - map(previewValues, (value, idx) => ( - {value.toString()} - )) - )} -
- - - - Sort + Query - - - - - Enable multiple values to be checked - - { - setVariableMultiSelect(e); - if (!e) { - setVariableShowALLOption(false); - } - }} +
+ setVariableQueryValue(e)} + height="240px" + options={{ + fontSize: 13, + wordWrap: 'on', + lineNumbers: 'off', + glyphMargin: false, + folding: false, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + minimap: { + enabled: false, + }, + }} + /> + +
+
+ )} + {queryType === 'CUSTOM' && ( + + { + setVariableCustomValue(e.target.value); + setPreviewValues( + sortValues( + commaValuesParser(e.target.value), + variableSortType, + ) as never, + ); + }} + /> + ), + }, + ]} /> - {variableMultiSelect && ( - + )} + {queryType === 'TEXTBOX' && ( + + + Default Value + + { + setVariableTextboxValue(e.target.value); + }} + placeholder="Enter a default value (if any)..." + style={{ width: 400 }} + /> + + )} + {(queryType === 'QUERY' || queryType === 'CUSTOM') && ( + <> + + + + Preview of Values + + +
+ {errorPreview ? ( + {errorPreview} + ) : ( + map(previewValues, (value, idx) => ( + {value.toString()} + )) + )} +
+
+ - Include an option for ALL values + Sort Values + + Sort the query output values + + + + + + + + + Enable multiple values to be checked + setVariableShowALLOption(e)} + checked={variableMultiSelect} + onChange={(e): void => { + setVariableMultiSelect(e); + if (!e) { + setVariableShowALLOption(false); + } + }} /> - )} - - )} + {variableMultiSelect && ( + + + + Include an option for ALL values + + + setVariableShowALLOption(e)} + /> + + )} + + )} +
-
- - -
-
+ ); } diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx index 4aa1bf9b22..70ffcb28a2 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -1,7 +1,6 @@ import '../DashboardSettings.styles.scss'; -import { blue, red } from '@ant-design/colors'; -import { MenuOutlined, PlusOutlined } from '@ant-design/icons'; +import { HolderOutlined, PlusOutlined } from '@ant-design/icons'; import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core'; import { DndContext, @@ -18,7 +17,7 @@ import { RowProps } from 'antd/lib'; import { convertVariablesToDbFormat } from 'container/NewDashboard/DashboardVariablesSelection/util'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; -import { PencilIcon, TrashIcon } from 'lucide-react'; +import { PenLine, Trash2 } from 'lucide-react'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -53,25 +52,33 @@ function TableRow({ children, ...props }: RowProps): JSX.Element { // eslint-disable-next-line react/jsx-props-no-spreading
{React.Children.map(children, (child) => { - if ((child as React.ReactElement).key === 'sort') { + if ((child as React.ReactElement).key === 'name') { return React.cloneElement(child as React.ReactElement, { children: ( - +
+ + {child} +
), }); } + return child; })}
); } -function VariablesSetting(): JSX.Element { +function VariablesSetting({ + variableViewModeRef, +}: { + variableViewModeRef: React.MutableRefObject<(() => void) | undefined>; +}): JSX.Element { const variableToDelete = useRef(null); const [deleteVariableModal, setDeleteVariableModal] = useState(false); @@ -111,6 +118,13 @@ function VariablesSetting(): JSX.Element { setVariableViewMode(viewType); }; + useEffect(() => { + if (variableViewModeRef) { + // eslint-disable-next-line no-param-reassign + variableViewModeRef.current = onDoneVariableViewMode; + } + }, [variableViewModeRef]); + const updateMutation = useUpdateDashboard(); useEffect(() => { @@ -245,47 +259,42 @@ function VariablesSetting(): JSX.Element { !existingVariableNamesMap[name]; const columns = [ - { - key: 'sort', - width: '10%', - }, { title: 'Variable', dataIndex: 'name', - width: '40%', + width: '50%', key: 'name', }, { title: 'Description', - dataIndex: 'description', - width: '35%', + width: '50%', key: 'description', - }, - { - title: 'Actions', - width: '15%', - key: 'action', render: (variable: IDashboardVariable): JSX.Element => ( - - - - +
+ + {variable.description} + + + + + +
), }, ]; @@ -353,6 +362,10 @@ function VariablesSetting(): JSX.Element { flexDirection: 'row', justifyContent: 'flex-end', padding: '0.5rem 0', + position: 'absolute', + top: '-56px', + right: '0px', + zIndex: '1', }} > + ), key: 'general', children: , }, - { label: 'Variables', key: 'variables', children: }, + { + label: ( + + ), + key: 'variables', + children: , + }, ]; - return ; + return ; } export default DashboardSettingsContent; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss index 9767c183c3..f610198f4a 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss @@ -1,16 +1,63 @@ -.variable-name { - font-size: 0.8rem; - min-width: 100px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: gray; -} - .variable-item { + display: flex; + align-items: center; + + .variable-name { + display: flex; + min-width: 56px; + height: 32px; + padding: 6px 6px 6px 8px; + align-items: center; + gap: 4px; + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + color: var(--bg-robin-300); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; /* 133.333% */ + } + + .variable-value { + display: flex; + min-width: 120px; + height: 32px; + padding: 6px 6px 6px 8px; + align-items: center; + gap: 4px; + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + border-left: none; + background: var(--bg-ink-400); + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; /* 133.333% */ + } + .variable-select { .ant-select-dropdown { max-width: 300px; } } } + +.lightMode { + .variable-item { + .variable-name { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + color: var(--bg-robin-300); + } + + .variable-value { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + color: var(--bg-ink-400); + } + } +} diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx index 1eed8f351f..0572196fbf 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx @@ -124,7 +124,7 @@ function DashboardVariableSelection(): JSX.Element | null { ); return ( - + {orderBasedSortedVariables && Array.isArray(orderBasedSortedVariables) && orderBasedSortedVariables.length > 0 && diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx index fdfb821267..2a14aa19e5 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx @@ -7,7 +7,7 @@ import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; import sortValues from 'lib/dashbaordVariables/sortVariableValues'; -import { debounce } from 'lodash-es'; +import { debounce, isArray, isString } from 'lodash-es'; import map from 'lodash-es/map'; import { memo, useEffect, useMemo, useState } from 'react'; import { useQuery } from 'react-query'; @@ -16,7 +16,7 @@ import { VariableResponseProps } from 'types/api/dashboard/variables/query'; import { popupContainer } from 'utils/selectPopupContainer'; import { variablePropsToPayloadVariables } from '../utils'; -import { SelectItemStyle, VariableContainer, VariableValue } from './styles'; +import { SelectItemStyle } from './styles'; import { areArraysEqual } from './util'; const ALL_SELECT_VALUE = '__ALL__'; @@ -108,10 +108,28 @@ function VariableItem({ if (!areArraysEqual(newOptionsData, oldOptionsData)) { /* eslint-disable no-useless-escape */ + + let valueNotInList = false; + + if (isArray(variableData.selectedValue)) { + variableData.selectedValue.forEach((val) => { + const isUsed = newOptionsData.includes(val); + + if (!isUsed) { + valueNotInList = true; + } + }); + } else if (isString(variableData.selectedValue)) { + const isUsed = newOptionsData.includes(variableData.selectedValue); + + if (!isUsed) { + valueNotInList = true; + } + } if ( variableData.type === 'QUERY' && variableData.name && - variablesToGetUpdated.includes(variableData.name) + (variablesToGetUpdated.includes(variableData.name) || valueNotInList) ) { let value = variableData.selectedValue; let allSelected = false; @@ -214,11 +232,11 @@ function VariableItem({ }, [variableData.type, variableData.customValue]); return ( - +
${variableData.name} - +
{variableData.type === 'TEXTBOX' ? ( )} - - +
+
); } diff --git a/frontend/src/container/NewDashboard/GridGraphs/index.tsx b/frontend/src/container/NewDashboard/GridGraphs/index.tsx index eff1d26baf..6ff37d6939 100644 --- a/frontend/src/container/NewDashboard/GridGraphs/index.tsx +++ b/frontend/src/container/NewDashboard/GridGraphs/index.tsx @@ -1,17 +1,17 @@ import GridGraphLayout from 'container/GridCardLayout'; -import ComponentsSlider from 'container/NewDashboard/ComponentsSlider'; -import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { FullScreenHandle } from 'react-full-screen'; import { GridComponentSliderContainer } from './styles'; -function GridGraphs(): JSX.Element { - const { isDashboardSliderOpen } = useDashboard(); +interface GridGraphsProps { + handle: FullScreenHandle; +} +function GridGraphs(props: GridGraphsProps): JSX.Element { + const { handle } = props; return ( - {isDashboardSliderOpen && } - - + ); } diff --git a/frontend/src/container/NewDashboard/index.tsx b/frontend/src/container/NewDashboard/index.tsx index ca10b6f89b..2042d77e16 100644 --- a/frontend/src/container/NewDashboard/index.tsx +++ b/frontend/src/container/NewDashboard/index.tsx @@ -1,12 +1,15 @@ +import { useFullScreenHandle } from 'react-full-screen'; + import Description from './DashboardDescription'; import GridGraphs from './GridGraphs'; function NewDashboard(): JSX.Element { + const handle = useFullScreenHandle(); return ( - <> - - - +
+ + +
); } diff --git a/frontend/src/container/NewExplorerCTA/config.ts b/frontend/src/container/NewExplorerCTA/config.ts index e5ccc110d3..c798e4fa51 100644 --- a/frontend/src/container/NewExplorerCTA/config.ts +++ b/frontend/src/container/NewExplorerCTA/config.ts @@ -8,4 +8,5 @@ export const buttonText: Record = { [ROUTES.LOGS_EXPLORER]: 'Switch to Old Logs Explorer', [ROUTES.TRACE]: 'Try new Traces Explorer', [ROUTES.OLD_LOGS_EXPLORER]: 'Switch to New Logs Explorer', + [ROUTES.TRACES_EXPLORER]: 'Switch to Old Trace Explorer', }; diff --git a/frontend/src/container/NewExplorerCTA/index.tsx b/frontend/src/container/NewExplorerCTA/index.tsx index 5b6e485193..c91151940c 100644 --- a/frontend/src/container/NewExplorerCTA/index.tsx +++ b/frontend/src/container/NewExplorerCTA/index.tsx @@ -14,7 +14,8 @@ function NewExplorerCTA(): JSX.Element | null { () => location.pathname === ROUTES.LOGS_EXPLORER || location.pathname === ROUTES.TRACE || - location.pathname === ROUTES.OLD_LOGS_EXPLORER, + location.pathname === ROUTES.OLD_LOGS_EXPLORER || + location.pathname === ROUTES.TRACES_EXPLORER, [location.pathname], ); @@ -25,6 +26,8 @@ function NewExplorerCTA(): JSX.Element | null { history.push(ROUTES.TRACES_EXPLORER); } else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) { history.push(ROUTES.LOGS_EXPLORER); + } else if (location.pathname === ROUTES.TRACES_EXPLORER) { + history.push(ROUTES.TRACE); } }, [location.pathname]); @@ -47,6 +50,10 @@ function NewExplorerCTA(): JSX.Element | null { return null; } + if (location.pathname === ROUTES.TRACES_EXPLORER) { + return button; + } + if (location.pathname === ROUTES.LOGS_EXPLORER) { return button; } diff --git a/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss index f9652e859a..cb19bf7e9e 100644 --- a/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss +++ b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss @@ -1,184 +1,184 @@ .explorer-columns-renderer { - margin-top: 10px; + margin-top: 10px; + margin-bottom: 30px; - .title { - display: flex; - align-items: center; - gap: 4px; - } + .title { + display: flex; + align-items: center; + gap: 4px; + padding-left: 16px; + } - .ant-typography { - color: var(rgba(255, 255, 255, 0.85)); - font-family: "Inter"; - font-size: 13px; - font-style: normal; - font-weight: 400; - line-height: 22px; - letter-spacing: 0.5px; - } + .ant-typography { + color: var(rgba(255, 255, 255, 0.85)); + font-family: 'Inter'; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 22px; + letter-spacing: 0.5px; + } - .ant-divider { - margin: 8px 0 !important; - border: 0.5px solid var(--bg-slate-400); - } + .ant-divider { + margin: 8px 0 !important; + border: 0.5px solid var(--bg-slate-400); + } - .explorer-columns-contents { - display: flex; - justify-content: space-between; - align-items: center; + .explorer-columns-contents { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 16px; + padding-right: 8px; - .explorer-columns { - display: flex; - align-items: center; - gap: 12px; - overflow-x: scroll; - min-width: 90%; + .explorer-columns { + display: flex; + align-items: center; + gap: 12px; + overflow-x: scroll; + min-width: 90%; - .explorer-columns-list { - display: flex !important; - } - - .explorer-column-card { - display: flex; - align-items: center; - justify-content: space-between; - padding: 4px; - min-width: 200px; - border-radius: 2px; - border: 1px solid var(--colorBorder, rgba(118, 136, 201, 0.12)); - background: var(--bg-slate-500); - cursor: unset; + .explorer-columns-list { + display: flex !important; + } - .explorer-column-title { - display: flex; - align-items: center; - gap: 8px; - font-family: Inter; - font-size: 12px; - cursor: grab; - } - - .lucide-trash2 { - cursor: pointer !important; - } + .explorer-column-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px; + min-width: 200px; + border-radius: 2px; + border: 1px solid var(--colorBorder, rgba(118, 136, 201, 0.12)); + background: var(--bg-slate-500); + cursor: unset; - } - } + .explorer-column-title { + display: flex; + align-items: center; + gap: 8px; + font-family: Inter; + font-size: 12px; + cursor: grab; + } - .explorer-columns::-webkit-scrollbar { - height: 0px; /* Height of the scrollbar */ - } + .lucide-trash2 { + cursor: pointer !important; + } + } + } - .action-btn { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - padding: 0px 16px; - border-radius: 2px; - background: var(--bg-robin-400); - } - } + .explorer-columns::-webkit-scrollbar { + height: 0px; /* Height of the scrollbar */ + } + + .action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0px 16px; + border-radius: 2px; + background: var(--bg-robin-400); + } + } } .explorer-columns-search { - border: 1px solid rgba(118, 136, 201, 0.12); - border-radius: 6px; - padding: 0px; - background:#141414; - > input { - height: 32px; - padding: 0 6px; - } - - + border: 1px solid rgba(118, 136, 201, 0.12); + border-radius: 6px; + padding: 0px; + background: #141414; + > input { + height: 32px; + padding: 0 6px; + } } .explorer-columns-dropdown { - height: 200px; - background-color: var(--bg-slate-500); - overflow: hidden !important; - .ant-dropdown-menu { - padding: 0; + height: 200px; + background-color: var(--bg-slate-500); + overflow: hidden !important; + .ant-dropdown-menu { + padding: 0; - .ant-dropdown-menu-item { - padding: 4px; - .ant-checkbox-wrapper { - padding: 2px 8px !important; - } + .ant-dropdown-menu-item { + padding: 4px; + .ant-checkbox-wrapper { + padding: 2px 8px !important; + } - .attribute-columns { - display: flex; - flex-direction: column; - height: 160px; - overflow: scroll; - } + .attribute-columns { + display: flex; + flex-direction: column; + height: 160px; + overflow: scroll; + } - .attribute-columns::-webkit-scrollbar { - width: 3px; /* Width of the scrollbar */ - } - - .attribute-columns::-webkit-scrollbar-track { - background: var(--bg-slate-500); /* Color of the track */ - } - - .attribute-columns::-webkit-scrollbar-thumb { - background: var(--bg-vanilla-400); /* Color of the thumb */ - border-radius: 4px; /* Roundness of the thumb */ - } - - .attribute-columns::-webkit-scrollbar-thumb:hover { - background: var(--bg-vanilla-300); /* Color of the thumb on hover */ - } - } - } + .attribute-columns::-webkit-scrollbar { + width: 3px; /* Width of the scrollbar */ + } + + .attribute-columns::-webkit-scrollbar-track { + background: var(--bg-slate-500); /* Color of the track */ + } + + .attribute-columns::-webkit-scrollbar-thumb { + background: var(--bg-vanilla-400); /* Color of the thumb */ + border-radius: 4px; /* Roundness of the thumb */ + } + + .attribute-columns::-webkit-scrollbar-thumb:hover { + background: var(--bg-vanilla-300); /* Color of the thumb on hover */ + } + } + } } .lightMode { - .explorer-columns-renderer { + .explorer-columns-renderer { + .ant-divider { + border: 0.5px solid var(--bg-vanilla-300); + } - .ant-divider { - border: 0.5px solid var(--bg-vanilla-300); - } + .explorer-columns { + .explorer-column-card { + border: 1px solid var(--colorBorder, rgba(118, 136, 201, 0.12)); + background: var(--bg-vanilla-200); + } + } - .explorer-columns { - .explorer-column-card { - border: 1px solid var(--colorBorder, rgba(118, 136, 201, 0.12)); - background: var(--bg-vanilla-200); - } - } + .explorer-columns-search { + border: 1px solid rgba(118, 136, 201, 0.12); + } + } - .explorer-columns-search { - border: 1px solid rgba(118, 136, 201, 0.12); - } - } + .explorer-columns-dropdown { + background-color: var(--bg-vanilla-100); - .explorer-columns-dropdown { - background-color: var(--bg-vanilla-100); + .ant-dropdown-menu-item { + .attribute-columns { + &::-webkit-scrollbar { + width: 3px; /* Width of the scrollbar */ + } - .ant-dropdown-menu-item { - .attribute-columns { - &::-webkit-scrollbar { - width: 3px; /* Width of the scrollbar */ - } - - &::-webkit-scrollbar-track { - background: var(--bg-vanilla-200); /* Color of the track */ - } - - &::-webkit-scrollbar-thumb { - background: var(--bg-vanilla-400); /* Color of the thumb */ - } - - &::-webkit-scrollbar-thumb:hover { - background: var(--bg-vanilla-300); /* Color of the thumb on hover */ - } - } - } - } + &::-webkit-scrollbar-track { + background: var(--bg-vanilla-200); /* Color of the track */ + } - .explorer-columns-search { - background: var(--bg-vanilla-100); - } + &::-webkit-scrollbar-thumb { + background: var(--bg-vanilla-400); /* Color of the thumb */ + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-vanilla-300); /* Color of the thumb on hover */ + } + } + } + } + + .explorer-columns-search { + background: var(--bg-vanilla-100); + } } diff --git a/frontend/src/container/NewWidget/LeftContainer/LeftContainer.styles.scss b/frontend/src/container/NewWidget/LeftContainer/LeftContainer.styles.scss new file mode 100644 index 0000000000..068ab2c92d --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/LeftContainer.styles.scss @@ -0,0 +1,16 @@ +.query-section-left-container { + border: none; + border-top: 1px solid var(--Slate-400, #1d212d); + background: var(--Ink-500, #0b0c0e); + + .ant-card-body { + padding: 0px; + } +} + +.lightMode { + .query-section-left-container { + border-top: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } +} diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/QueryHeader.styles.scss b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/QueryHeader.styles.scss index 019344bbe0..4da11bc8e2 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/QueryHeader.styles.scss +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/QueryHeader.styles.scss @@ -1,7 +1,8 @@ .query-header-container { - .action-btn { - display: flex; - align-items: center; - justify-content: center; - } + padding: 0px 8px 0px 16px; + .action-btn { + display: flex; + align-items: center; + justify-content: center; + } } diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx index 075bc6799e..4ab40444f4 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx @@ -21,7 +21,11 @@ function ClickHouseQueryContainer(): JSX.Element | null { queryData={q} /> ))} - }> + } + style={{ margin: '0.4rem 1rem' }} + > Query diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx index f12b150bd3..7dd61595d6 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx @@ -86,12 +86,9 @@ function ClickHouseQueryBuilder({ colors: { 'editor.background': Color.BG_INK_300, }, - // fontFamily: 'SF Mono', - fontFamily: 'Space Mono', - fontSize: 20, - fontWeight: 'normal', - lineHeight: 18, - letterSpacing: -0.06, + }); + document.fonts.ready.then(() => { + monaco.editor.remeasureFonts(); }); } diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/index.tsx index 3fe8b21082..1a254a3670 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/index.tsx @@ -25,7 +25,11 @@ function PromQLQueryContainer(): JSX.Element | null { /> ), )} - }> + } + style={{ margin: '0.4rem 1rem' }} + > Query diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QuerySection.styles.scss b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QuerySection.styles.scss index d6ae43ac9a..b7cd213ae1 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QuerySection.styles.scss +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QuerySection.styles.scss @@ -8,6 +8,15 @@ display: flex; align-items: center; justify-content: center; + gap: 4px; + color: #fff; + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: -0.06px; + padding: 7px 23px; .prom-ql-icon { height: 14px; @@ -17,6 +26,7 @@ } .ant-btn-default { border-color: transparent; + box-shadow: none; } } .ant-tabs-tab-active { @@ -27,16 +37,26 @@ .ant-tabs-nav { margin: 0px; - margin-bottom: 0.5rem; + + .ant-tabs-nav-wrap { + padding: 8px 16px; + } + + .ant-tabs-extra-content { + padding-right: 8px; + } } .ant-tabs-nav::before { border-bottom: none !important; } .ant-tabs-nav-list { - border: 1px solid var(--bg-slate-200); + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-400); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); } .ant-tabs-tab + .ant-tabs-tab { - border-left: 1px solid var(--bg-slate-200) !important; + border-left: 1px solid var(--bg-slate-400) !important; } .stage-run-query { display: flex; @@ -46,11 +66,16 @@ .lightMode { .dashboard-navigation { + .nav-btns { + color: var(---bg-ink-300); + background-color: var(--bg-vanilla-200); + } .ant-tabs-nav-list { border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); } .ant-tabs-tab + .ant-tabs-tab { - border-left: 1px solid var(--bg-vanilla-200) !important; + border-left: 1px solid var(--bg-vanilla-100) !important; } .ant-tabs-tab-active { .nav-btns { diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx index 11f01b402f..beed344d8b 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -147,11 +147,10 @@ function QuerySection({ { key: EQueryType.QUERY_BUILDER, label: ( - - - + ), tab: Query Builder, children: ( @@ -169,11 +168,10 @@ function QuerySection({ { key: EQueryType.QUERY_BUILDER, label: ( - - - + ), tab: Query Builder, children: ( @@ -187,11 +185,10 @@ function QuerySection({ { key: EQueryType.CLICKHOUSE, label: ( - - - + ), tab: ClickHouse Query, children: , @@ -204,6 +201,7 @@ function QuerySection({ + PromQL ), diff --git a/frontend/src/container/NewWidget/LeftContainer/QueryTypeTag.tsx b/frontend/src/container/NewWidget/LeftContainer/QueryTypeTag.tsx index d119bb7c27..e21fc9c4a4 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QueryTypeTag.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QueryTypeTag.tsx @@ -1,28 +1,14 @@ import { EQueryType } from 'types/common/dashboard'; -import { Tag } from '../styles'; - function QueryTypeTag({ queryType }: IQueryTypeTagProps): JSX.Element { switch (queryType) { case EQueryType.QUERY_BUILDER: - return ( - - Query Builder - - ); + return Query Builder; case EQueryType.CLICKHOUSE: - return ( - - ClickHouse Query - - ); + return ClickHouse Query; case EQueryType.PROM: - return ( - - PromQL - - ); + return PromQL; default: return ; } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx index 99da2e517e..c28b453ac5 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx @@ -1,8 +1,8 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; +import { Spline } from 'lucide-react'; import { EQueryType } from 'types/common/dashboard'; import QueryTypeTag from '../QueryTypeTag'; -import { PlotTagWrapperStyled } from './styles'; interface IPlotTagProps { queryType: EQueryType; @@ -15,9 +15,10 @@ function PlotTag({ queryType, panelType }: IPlotTagProps): JSX.Element | null { } return ( - - Plotted using - +
+ + Plotted with +
); } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.styles.scss b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.styles.scss new file mode 100644 index 0000000000..26c99e5a37 --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.styles.scss @@ -0,0 +1,42 @@ +.widget-graph { + border: none; + background-color: unset; + background-image: radial-gradient(var(--bg-slate-400) 1px, transparent 0); + background-size: 20px 20px; + padding: 16px; + + .header { + display: flex; + align-items: center; + justify-content: space-between; + + .plot-tag { + display: inline-flex; + padding: 4px 4px 4px 6px; + align-items: center; + gap: 6px; + border-radius: 4px; + background: var(--Slate-400, #1d212d); + backdrop-filter: blur(6px); + width: fit-content; + } + } + + .header:has(.date-time-selector:only-child) { + justify-content: end; + } +} + +.lightMode { + .widget-graph { + background-color: var(--bg-vanilla-100); + background-image: radial-gradient(var(--bg-vanilla-400) 1px, transparent 0); + background-size: 20px 20px; + + .header { + .plot-tag { + background: var(--bg-vanilla-300); + } + } + } +} diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx index d61a7f3fc6..d0b69fcd8d 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx @@ -40,7 +40,7 @@ function WidgetGraphContainer({ if ( selectedGraph !== PANEL_TYPES.LIST && - queryResponse.data?.payload.data.result.length === 0 + queryResponse.data?.payload.data?.result?.length === 0 ) { return ( @@ -50,7 +50,7 @@ function WidgetGraphContainer({ } if ( selectedGraph === PANEL_TYPES.LIST && - queryResponse.data?.payload.data.newResult.data.result.length === 0 + queryResponse.data?.payload?.data?.newResult?.data?.result?.length === 0 ) { return ( diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx index 72440014f1..9262a4a766 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx @@ -2,6 +2,7 @@ import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; import PanelWrapper from 'container/PanelWrapper/PanelWrapper'; import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config'; +import { useIsDarkMode } from 'hooks/useDarkMode'; import useUrlQuery from 'hooks/useUrlQuery'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import GetMinMax from 'lib/getMinMax'; @@ -84,8 +85,24 @@ function WidgetGraph({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const isDarkMode = useIsDarkMode(); + return ( -
+
Invalid widget; + return ( + + Invalid widget + + ); } return ( - - + +
+ + +
{queryResponse.error && ( diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts index a5d030e27c..e671187eac 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts @@ -12,13 +12,10 @@ export const Container = styled(Card)` } .ant-card-body { - padding: ${({ $panelType }): string => - $panelType === PANEL_TYPES.TABLE || $panelType === PANEL_TYPES.LIST - ? '0 0' - : '1.5rem 0'}; height: 60vh; display: flex; flex-direction: column; + padding: 0px; } `; diff --git a/frontend/src/container/NewWidget/LeftContainer/index.tsx b/frontend/src/container/NewWidget/LeftContainer/index.tsx index 66d91b7544..e7c149c246 100644 --- a/frontend/src/container/NewWidget/LeftContainer/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/index.tsx @@ -1,3 +1,5 @@ +import './LeftContainer.styles.scss'; + import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; @@ -97,7 +99,7 @@ function LeftContainer({ setRequestData={setRequestData} selectedWidget={selectedWidget} /> - + {selectedGraph === PANEL_TYPES.LIST && ( >; +} + +export function ColumnUnitSelector( + props: ColumnUnitSelectorProps, +): JSX.Element { + const { currentQuery } = useQueryBuilder(); + + function getAggregateColumnsNamesAndLabels(): string[] { + return currentQuery.builder.queryData.map((q) => q.queryName); + } + + const { columnUnits, setColumnUnits } = props; + const aggregationQueries = getAggregateColumnsNamesAndLabels(); + + function handleColumnUnitSelect(queryName: string, value: string): void { + setColumnUnits((prev) => ({ + ...prev, + [queryName]: value, + })); + } + return ( +
+ Column Units + {aggregationQueries.map((query) => ( + handleColumnUnitSelect(query, value)} + fieldLabel={query} + key={query} + handleClear={(): void => { + handleColumnUnitSelect(query, ''); + }} + /> + ))} +
+ ); +} diff --git a/frontend/src/container/NewWidget/RightContainer/RightContainer.styles.scss b/frontend/src/container/NewWidget/RightContainer/RightContainer.styles.scss new file mode 100644 index 0000000000..ebcbbedbbc --- /dev/null +++ b/frontend/src/container/NewWidget/RightContainer/RightContainer.styles.scss @@ -0,0 +1,470 @@ +.right-container { + display: flex; + flex-direction: column; + + .header { + display: flex; + padding: 14px 14px 14px 12px; + align-items: center; + gap: 8px; + + .purple-dot { + width: 8px; + height: 8px; + border-radius: 2px; + background: var(--bg-robin-400); + } + .header-text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + + .name-description { + display: flex; + flex-direction: column; + padding: 12px 12px 16px 12px; + border-top: 1px solid var(--bg-slate-500); + border-bottom: 1px solid var(--bg-slate-500); + gap: 8px; + + .typography { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 138.462% */ + letter-spacing: 0.52px; + text-transform: uppercase; + } + + .name-input { + display: flex; + padding: 6px 6px 6px 8px; + align-items: center; + gap: 4px; + flex: 1 0 0; + align-self: stretch; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + margin-bottom: 16px; + } + + .description-input { + border-style: unset; + .ant-input { + display: flex; + height: 80px; + padding: 6px 6px 6px 8px; + align-items: flex-start; + gap: 4px; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + } + } + + .panel-config { + display: flex; + flex-direction: column; + padding: 12px 12px 16px 12px; + gap: 8px; + border-bottom: 1px solid var(--bg-slate-500); + + .typography { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 138.462% */ + letter-spacing: 0.52px; + text-transform: uppercase; + } + + .panel-type-select { + .ant-select-selector { + display: flex; + height: 32px; + padding: 6px 6px 6px 8px; + align-items: center; + gap: 4px; + flex-shrink: 0; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + } + + .select-option { + display: flex; + align-items: center; + gap: 6px; + .icon { + display: flex; + align-items: center; + } + + .display { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; /* 133.333% */ + } + } + } + + .fill-gaps { + margin-top: 16px; + display: flex; + padding: 12px; + justify-content: space-between; + align-items: center; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-400); + + .fill-gaps-text { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 138.462% */ + letter-spacing: 0.52px; + text-transform: uppercase; + } + } + + .panel-time-text { + margin-top: 16px; + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 138.462% */ + letter-spacing: 0.52px; + text-transform: uppercase; + } + + .y-axis-unit-selector { + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 8px; + + .heading { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 138.462% */ + letter-spacing: 0.52px; + text-transform: uppercase; + } + + .input { + display: flex; + height: 32px; + padding: 6px 6px 6px 8px; + align-items: center; + gap: 4px; + align-self: stretch; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + + .ant-input { + background: var(--bg-ink-300); + } + } + } + .soft-min-max { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 4px; + gap: 12px; + + .container { + display: flex; + height: 32px; + align-items: center; + width: 50%; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-400); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + + .text { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; /* 133.333% */ + letter-spacing: 0.48px; + text-transform: uppercase; + width: 60%; + padding: 8px; + } + .input { + width: 50%; + border: none; + border-left: 1px solid var(--bg-slate-400); + } + } + } + + .bucket-config { + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 8px; + + .label { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 138.462% */ + letter-spacing: 0.52px; + text-transform: uppercase; + } + + .bucket-size-label { + margin-top: 8px; + } + + .bucket-input { + display: flex; + width: 100%; + height: 32px; + padding: 6px 6px 6px 8px; + align-items: center; + gap: 4px; + align-self: stretch; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + + .ant-input { + background: var(--bg-ink-300); + } + } + + .combine-hist { + display: flex; + justify-content: space-between; + margin-top: 8px; + + .label { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 138.462% */ + letter-spacing: 0.52px; + text-transform: uppercase; + } + } + } + } + + .alerts { + display: flex; + padding: 12px; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--bg-slate-500); + cursor: pointer; + + .left-section { + display: flex; + align-items: center; + gap: 8px; + + .bell-icon { + color: var(--bg-vanilla-400); + } + + .alerts-text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: 0.14px; + } + } + .plus-icon { + color: var(--bg-vanilla-400); + } + } +} + +.select-option { + display: flex; + align-items: center; + gap: 6px; + .icon { + display: flex; + align-items: center; + } + + .display { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; /* 133.333% */ + } +} + +.lightMode { + .right-container { + background-color: var(--bg-vanilla-100); + .header { + .header-text { + color: var(--bg-ink-400); + } + } + + .name-description { + border-top: 1px solid var(--bg-vanilla-300); + border-bottom: 1px solid var(--bg-vanilla-300); + + .typography { + color: var(--bg-ink-400); + } + + .name-input { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + color: var(--bg-ink-300); + } + + .description-input { + .ant-input { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + color: var(--bg-ink-300); + } + } + } + + .panel-config { + border-bottom: 1px solid var(--bg-vanilla-300); + + .typography { + color: var(--bg-ink-400); + } + + .panel-type-select { + .ant-select-selector { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + } + + .select-option { + .display { + color: var(--bg-ink-300); + } + } + } + + .fill-gaps { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + + .fill-gaps-text { + color: var(--bg-ink-400); + } + } + + .panel-time-text { + color: var(--bg-ink-400); + } + + .y-axis-unit-selector { + .heading { + color: var(--bg-ink-400); + } + + .input { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + + .ant-input { + background: var(--bg-vanilla-300); + } + } + } + .soft-min-max { + .container { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + + .text { + color: var(--bg-ink-300); + } + .input { + border-left: 1px solid var(--bg-vanilla-300); + } + } + } + } + + .alerts { + border-bottom: 1px solid var(--bg-vanilla-300); + + .left-section { + .bell-icon { + color: var(--bg-ink-300); + } + + .alerts-text { + color: var(--bg-ink-300); + } + } + .plus-icon { + color: var(--bg-ink-300); + } + } + } + + .select-option { + .display { + color: var(--bg-ink-100); + } + } +} diff --git a/frontend/src/container/NewWidget/RightContainer/Threshold/ColorSelector.styles.scss b/frontend/src/container/NewWidget/RightContainer/Threshold/ColorSelector.styles.scss index a2df2ed12b..534a4b2e21 100644 --- a/frontend/src/container/NewWidget/RightContainer/Threshold/ColorSelector.styles.scss +++ b/frontend/src/container/NewWidget/RightContainer/Threshold/ColorSelector.styles.scss @@ -1,7 +1,27 @@ -.color-selector-button { - border: none; +.color-selector-space { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; } -.color-selector-light { - border: 1px solid #d9d9d9; +.color-selector-button { + border: none; + width: 100%; + + .ant-btn { + box-shadow: none; + background-color: unset; + } +} + +.lightMode { + .color-selector-button { + background-color: var(--bg-vanilla-300); + + .ant-btn { + box-shadow: none; + background-color: unset; + } + } } diff --git a/frontend/src/container/NewWidget/RightContainer/Threshold/ColorSelector.tsx b/frontend/src/container/NewWidget/RightContainer/Threshold/ColorSelector.tsx index 2640e3c867..60bcbbdf12 100644 --- a/frontend/src/container/NewWidget/RightContainer/Threshold/ColorSelector.tsx +++ b/frontend/src/container/NewWidget/RightContainer/Threshold/ColorSelector.tsx @@ -4,7 +4,6 @@ import { DownOutlined } from '@ant-design/icons'; import { Button, ColorPicker, Dropdown, Space } from 'antd'; import { Color } from 'antd/es/color-picker'; import { MenuProps } from 'antd/lib'; -import { useIsDarkMode } from 'hooks/useDarkMode'; import useDebounce from 'hooks/useDebounce'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; @@ -18,8 +17,6 @@ function ColorSelector({ const debounceColor = useDebounce(colorFromPicker); - const isDarkMode = useIsDarkMode(); - useEffect(() => { if (debounceColor) { setColor(debounceColor); @@ -69,11 +66,9 @@ function ColorSelector({
-
- - {selectedGraph === PANEL_TYPES.TIME_SERIES && ( - - Label - {isEditMode ? ( - - ) : ( - - )} - + )} +
+ {selectedGraph === PANEL_TYPES.TIME_SERIES && ( +
+ Label + {isEditMode ? ( + + ) : ( + )} - {(selectedGraph === PANEL_TYPES.VALUE || - selectedGraph === PANEL_TYPES.TABLE) && ( - <> - - If value {selectedGraph === PANEL_TYPES.TABLE ? 'in' : 'is'} - - {isEditMode ? ( - <> - {selectedGraph === PANEL_TYPES.TABLE && ( - - - - ) : ( - <> - {selectedGraph === PANEL_TYPES.TABLE && ( - - - is - - )} - - + is + )} - - )} - -
-
- - {isEditMode ? ( - + - ) : ( - - )} - -
-
- - Show with - - {isEditMode ? ( - <> - - + ) : ( + + )} +
+
+ {isEditMode ? ( + <> +
+ +
+ + - +
); } export default YAxisUnitSelector; + +YAxisUnitSelector.defaultProps = { + handleClear: (): void => {}, +}; diff --git a/frontend/src/container/NewWidget/RightContainer/constants.ts b/frontend/src/container/NewWidget/RightContainer/constants.ts index 0a4b250e70..5276fc2bcd 100644 --- a/frontend/src/container/NewWidget/RightContainer/constants.ts +++ b/frontend/src/container/NewWidget/RightContainer/constants.ts @@ -28,6 +28,7 @@ export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = { [PANEL_TYPES.LIST]: false, [PANEL_TYPES.PIE]: false, [PANEL_TYPES.BAR]: true, + [PANEL_TYPES.HISTOGRAM]: false, [PANEL_TYPES.TRACE]: false, [PANEL_TYPES.EMPTY_WIDGET]: false, } as const; @@ -39,6 +40,7 @@ export const panelTypeVsSoftMinMax: { [key in PANEL_TYPES]: boolean } = { [PANEL_TYPES.LIST]: false, [PANEL_TYPES.PIE]: false, [PANEL_TYPES.BAR]: true, + [PANEL_TYPES.HISTOGRAM]: false, [PANEL_TYPES.TRACE]: false, [PANEL_TYPES.EMPTY_WIDGET]: false, } as const; @@ -50,6 +52,7 @@ export const panelTypeVsDragAndDrop: { [key in PANEL_TYPES]: boolean } = { [PANEL_TYPES.PIE]: false, [PANEL_TYPES.LIST]: false, [PANEL_TYPES.BAR]: false, + [PANEL_TYPES.HISTOGRAM]: false, [PANEL_TYPES.TRACE]: false, [PANEL_TYPES.EMPTY_WIDGET]: false, } as const; @@ -61,6 +64,7 @@ export const panelTypeVsFillSpan: { [key in PANEL_TYPES]: boolean } = { [PANEL_TYPES.LIST]: false, [PANEL_TYPES.PIE]: false, [PANEL_TYPES.BAR]: false, + [PANEL_TYPES.HISTOGRAM]: false, [PANEL_TYPES.TRACE]: false, [PANEL_TYPES.EMPTY_WIDGET]: false, } as const; @@ -68,10 +72,11 @@ export const panelTypeVsFillSpan: { [key in PANEL_TYPES]: boolean } = { export const panelTypeVsYAxisUnit: { [key in PANEL_TYPES]: boolean } = { [PANEL_TYPES.TIME_SERIES]: true, [PANEL_TYPES.VALUE]: true, - [PANEL_TYPES.TABLE]: true, + [PANEL_TYPES.TABLE]: false, [PANEL_TYPES.LIST]: false, [PANEL_TYPES.PIE]: false, [PANEL_TYPES.BAR]: true, + [PANEL_TYPES.HISTOGRAM]: false, [PANEL_TYPES.TRACE]: false, [PANEL_TYPES.EMPTY_WIDGET]: false, } as const; @@ -83,6 +88,19 @@ export const panelTypeVsCreateAlert: { [key in PANEL_TYPES]: boolean } = { [PANEL_TYPES.LIST]: false, [PANEL_TYPES.PIE]: false, [PANEL_TYPES.BAR]: true, + [PANEL_TYPES.HISTOGRAM]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; + +export const panelTypeVsBucketConfig: { [key in PANEL_TYPES]: boolean } = { + [PANEL_TYPES.TIME_SERIES]: false, + [PANEL_TYPES.VALUE]: false, + [PANEL_TYPES.TABLE]: false, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.PIE]: false, + [PANEL_TYPES.BAR]: false, + [PANEL_TYPES.HISTOGRAM]: true, [PANEL_TYPES.TRACE]: false, [PANEL_TYPES.EMPTY_WIDGET]: false, } as const; @@ -96,6 +114,21 @@ export const panelTypeVsPanelTimePreferences: { [PANEL_TYPES.LIST]: false, [PANEL_TYPES.PIE]: true, [PANEL_TYPES.BAR]: true, + [PANEL_TYPES.HISTOGRAM]: false, [PANEL_TYPES.TRACE]: false, [PANEL_TYPES.EMPTY_WIDGET]: false, } as const; + +export const panelTypeVsColumnUnitPreferences: { + [key in PANEL_TYPES]: boolean; +} = { + [PANEL_TYPES.TIME_SERIES]: false, + [PANEL_TYPES.VALUE]: false, + [PANEL_TYPES.TABLE]: true, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.PIE]: false, + [PANEL_TYPES.BAR]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.HISTOGRAM]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx index d00dcb1130..f615d37829 100644 --- a/frontend/src/container/NewWidget/RightContainer/index.tsx +++ b/frontend/src/container/NewWidget/RightContainer/index.tsx @@ -1,15 +1,8 @@ -import { UploadOutlined } from '@ant-design/icons'; -import { - Button, - Divider, - Input, - InputNumber, - Select, - Space, - Switch, - Typography, -} from 'antd'; -import InputComponent from 'components/Input'; +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import './RightContainer.styles.scss'; + +import { Input, InputNumber, Select, Space, Switch, Typography } from 'antd'; import TimePreference from 'components/TimePreferenceDropDown'; import { PANEL_TYPES } from 'constants/queryBuilder'; import GraphTypes, { @@ -17,6 +10,7 @@ import GraphTypes, { } from 'container/NewDashboard/ComponentsSlider/menuItems'; import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { ConciergeBell, Plus } from 'lucide-react'; import { Dispatch, SetStateAction, @@ -24,10 +18,13 @@ import { useEffect, useState, } from 'react'; -import { Widgets } from 'types/api/dashboard/getAll'; +import { ColumnUnit, Widgets } from 'types/api/dashboard/getAll'; import { DataSource } from 'types/common/queryBuilder'; +import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector'; import { + panelTypeVsBucketConfig, + panelTypeVsColumnUnitPreferences, panelTypeVsCreateAlert, panelTypeVsFillSpan, panelTypeVsPanelTimePreferences, @@ -35,7 +32,6 @@ import { panelTypeVsThreshold, panelTypeVsYAxisUnit, } from './constants'; -import { Container, Title } from './styles'; import ThresholdSelector from './Threshold/ThresholdSelector'; import { ThresholdProps } from './Threshold/types'; import { timePreferance } from './timeItems'; @@ -50,12 +46,18 @@ function RightContainer({ setTitle, title, selectedGraph, + bucketCount, + bucketWidth, + setBucketCount, + setBucketWidth, setSelectedTime, selectedTime, yAxisUnit, setYAxisUnit, setGraphHandler, thresholds, + combineHistogram, + setCombineHistogram, setThresholds, selectedWidget, isFillSpans, @@ -64,6 +66,8 @@ function RightContainer({ softMin, setSoftMax, setSoftMin, + columnUnits, + setColumnUnits, }: RightContainerProps): JSX.Element { const onChangeHandler = useCallback( (setFunc: Dispatch>, value: string) => { @@ -82,9 +86,13 @@ function RightContainer({ const allowFillSpans = panelTypeVsFillSpan[selectedGraph]; const allowYAxisUnit = panelTypeVsYAxisUnit[selectedGraph]; const allowCreateAlerts = panelTypeVsCreateAlert[selectedGraph]; + const allowBucketConfig = panelTypeVsBucketConfig[selectedGraph]; const allowPanelTimePreference = panelTypeVsPanelTimePreferences[selectedGraph]; + const allowPanelColumnPreference = + panelTypeVsColumnUnitPreferences[selectedGraph]; + const { currentQuery } = useQueryBuilder(); const [graphTypes, setGraphTypes] = useState(GraphTypes); @@ -118,66 +126,78 @@ function RightContainer({ ); return ( - - Panel Type - - Panel Attributes +
+
+
+ Panel details +
+
+ Name + onChangeHandler(setTitle, event.target.value)} + value={title} + rootClassName="name-input" + /> + Description +