From b232ecf01cc493e3c358f723cb7895149edf4457 Mon Sep 17 00:00:00 2001 From: Abhi kumar Date: Thu, 3 Sep 2026 10:38:20 +0000 Subject: [PATCH 1/4] chore(dashboard): remove dead V1 dashboard code (#12647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > **Stacked PR — merge bottom-up.** This is part of a stack retiring the V1 dashboard frontend. > > | | PR | Change | > |---|---|---| > | 1 | #12647 | delete dead V1 dashboard code | > | 2 | #12648 | retire the V1 panel editor and its widget route | > | 3 | #12649 | legacy notice for unmigrated public dashboards | > | 4 | #12650 | retire the V1 dashboard store | > | 5 | #12651 | move the shared chart layer to `lib/visualization` | > | 6 | #12652 | consolidate the widget-card stack under `container/WidgetCard` | > | 7 | #12653 | split `types/api/dashboard/getAll` | > | 8 | #12654 | drop the `V2` suffix | > > `CODEOWNERS` for all of the above is split out into **#12707**, which is *not* part of this stack (based on `main`) and should merge after it. --- #### Description V2 dashboards serve `/dashboard` and `/dashboard/:id` unconditionally — there is no feature flag — so the V1 page bodies and everything reachable only from them are dead. - Point the routes straight at the V2 pages and delete the V1 shims. - Delete `container/ListOfDashboard`, the non-`visualization` half of `container/DashboardContainer`, and the `GridCardLayout` grid shell. These are mutually entangled (`ListOfDashboard` ↔ `DashboardDescription` is an import cycle), so they have to go together. - Rescue the shared code that lived inside them before deleting: the variable dependency graph → `lib/dashboardVariables/dependencyGraph`, `uniqueOptions` → `NewSelect`, panel-type items → `GridCardLayout/panelTypeItems`. - Drop two menu affordances that no live caller could render: panel **Delete**/**Clone** (no consumer lists them in `headerMenuList`) and the **Dashboard Variables** drilldown submenu (route-gated to `/dashboard/:id`, which V2 serves through its own drilldown). - V2's variable cycle detection now uses V2's own dependency helpers instead of casting its form model into V1 types. #### Additional Information `container/DashboardContainer/visualization` is deliberately untouched — it is the shared chart layer, not V1 code. It moves later in the stack. **Verification:** `tsgo --noEmit`, `lint`, `jest` (766 suites / 7491 tests), `build`, and `knip` (unused files 51 → 41, zero newly orphaned) all clean. --- frontend/src/AppRoutes/pageComponents.ts | 4 +- .../dashboard/public/createPublicDashboard.ts | 36 - .../public/getPublicDashboardMeta.ts | 27 - .../public/revokePublicDashboardAccess.ts | 29 - .../dashboard/public/updatePublicDashboard.ts | 36 - frontend/src/api/v1/dashboards/create.ts | 23 - frontend/src/api/v1/dashboards/getAll.ts | 19 - frontend/src/api/v1/dashboards/id/delete.ts | 21 - frontend/src/api/v1/dashboards/id/lock.ts | 23 - .../src/assets/CustomIcons/ApacheIcon.tsx | 176 --- .../src/assets/CustomIcons/DockerIcon.tsx | 28 - .../assets/CustomIcons/ElasticSearchIcon.tsx | 36 - .../src/assets/CustomIcons/HerokuIcon.tsx | 27 - .../src/assets/CustomIcons/KubernetesIcon.tsx | 22 - .../src/assets/CustomIcons/MongoDBIcon.tsx | 68 -- frontend/src/assets/CustomIcons/MySQLIcon.tsx | 28 - frontend/src/assets/CustomIcons/NginxIcon.tsx | 22 - .../src/assets/CustomIcons/PostgreSQLIcon.tsx | 40 - frontend/src/assets/CustomIcons/RedisIcon.tsx | 60 - .../VariableItem.integration.test.tsx | 585 --------- frontend/src/components/NewSelect/utils.ts | 19 +- .../Description.styles.scss | 440 ------- .../SettingsDrawer/SettingsDrawer.styles.scss | 43 - .../SettingsDrawer/index.tsx | 34 - .../__tests__/DashboardDescription.test.tsx | 235 ---- .../DashboardDescription/index.tsx | 621 ---------- .../DashboardDescription/types.ts | 8 - .../DashboardDescription/utils.ts | 40 - .../DashboardSettings.styles.scss | 123 -- .../DashboardSettingsContent.styles.scss | 62 - .../DynamicVariable.styles.scss | 31 - .../DynamicVariable/DynamicVariable.tsx | 230 ---- .../__test__/DynamicVariable.test.tsx | 382 ------ .../VariableItem/VariableItem.styles.scss | 420 ------- .../VariableItem/VariableItem.test.tsx | 828 ------------- .../VariableItem/VariableItem.tsx | 851 ------------- .../VariableItem/WidgetSelector.tsx | 64 - .../VariableItem/styles.ts | 11 - .../addTagFiltersToDashboard.test.tsx | 328 ----- .../addTagFiltersToDashboard.tsx | 336 ----- .../DashboardVariableSettings/index.tsx | 537 -------- .../DashboardVariableSettings/types.ts | 7 - .../General/AddBadges/AddBadges.module.scss | 49 - .../General/AddBadges/index.tsx | 115 -- .../General/GeneralSettings.module.scss | 227 ---- .../DashboardSettings/General/index.tsx | 299 ----- .../DashboardSettings/General/styles.ts | 20 - .../DashboardSettings/General/utils.tsx | 22 - .../PublicDashboard.styles.scss | 109 -- .../__tests__/PublicDashboard.test.tsx | 434 ------- .../PublicDashboard/index.tsx | 378 ------ .../DashboardSettings/index.tsx | 79 -- .../CustomVariableInput.tsx | 69 -- .../DashboardVariableSelection.styles.scss | 113 -- .../DashboardVariableSelection.tsx | 181 --- .../DynamicVariableInput.tsx | 327 ----- .../QueryVariableInput.tsx | 267 ---- .../SelectVariableInput.tsx | 150 --- .../TextboxVariableInput.tsx | 85 -- .../VariableItem.test.tsx | 154 --- .../VariableItem.tsx | 76 -- .../DashboardVariableSelection.test.tsx | 388 ------ .../__test__/DynamicVariableInput.test.tsx | 266 ---- .../__test__/QueryVariableInput.test.tsx | 274 ----- .../VariableItem.defaulting.behavior.test.tsx | 129 -- .../__test__/dashboardVariables.test.tsx | 236 ---- .../__test__/mock.ts | 218 ---- .../DashboardVariablesSelection/index.tsx | 3 - .../DashboardVariablesSelection/styles.ts | 46 - .../useDashboardVariableSelectHelper.ts | 256 ---- .../useDashboardVariableUpdate.ts | 254 ---- .../DashboardVariablesSelection/util.ts | 484 -------- .../DashboardVariablesSelection/utils.test.ts | 531 -------- .../customVariableSelectStrategy.tsx | 5 - .../defaultVariableSelectStrategy.tsx | 20 - .../dynamicVariableSelectStrategy.tsx | 37 - .../queryVariableSelectStrategy.tsx | 5 - .../variableSelectStrategyTypes.ts | 11 - .../variableSelectStrategyUtils.ts | 32 - .../DashboardContainer/GridGraphs/index.tsx | 20 - .../DashboardContainer/GridGraphs/styles.ts | 28 - .../PanelTypeSelectionModal.styles.scss | 118 -- .../PanelTypeSelectionModal/index.tsx | 72 -- .../DynamicVariableDefaultBehavior.test.tsx | 525 -------- .../__test__/IntegrationTests.test.tsx | 152 --- .../__test__/PanelManagement.test.tsx | 508 -------- .../__test__/VariableItem.test.tsx | 242 ---- .../DashboardBreadcrumbs.styles.scss | 63 - .../DashboardHeader/DashboardBreadcrumbs.tsx | 64 - .../DashboardHeader.styles.scss | 9 - .../DashboardHeader/DashboardHeader.tsx | 17 - .../container/DashboardContainer/index.tsx | 16 - .../src/container/DashboardContainer/utils.ts | 18 - .../DashboardEmptyState.styles.scss | 176 --- .../DashboardEmptyState.tsx | 158 --- .../GridCard/FullView/PanelTypeSelector.tsx | 2 +- .../GridCard/WidgetGraphComponent.test.tsx | 2 - .../GridCard/WidgetGraphComponent.tsx | 155 +-- .../GridCardLayout/GridCardLayout.tsx | 607 ---------- .../container/GridCardLayout/WidgetRow.tsx | 128 -- .../src/container/GridCardLayout/index.tsx | 20 - .../panelTypeItems.tsx} | 0 .../DashboardActions.module.scss | 36 - .../ListOfDashboard/DashboardList.styles.scss | 1078 ----------------- .../DashboardTemplatesContent.tsx | 228 ---- .../DashboardTemplatesModal.styles.scss | 196 --- .../DashboardTemplatesModal.tsx | 34 - .../ListOfDashboard/DashboardsList.tsx | 1051 ---------------- .../ImportJSON/importJSON.styles.scss | 60 - .../ListOfDashboard/ImportJSON/index.tsx | 264 ---- .../ListOfDashboard/RequestDashboardBtn.tsx | 101 -- .../TableComponents/DeleteButton.tsx | 174 --- .../src/container/ListOfDashboard/index.tsx | 3 - .../src/container/ListOfDashboard/utils.ts | 55 - .../VisualizationSettingsSection.tsx | 2 +- .../NewWidget/RightContainer/index.tsx | 2 +- .../Drilldown/__tests__/Breakout.test.tsx | 10 - .../__tests__/TableDrilldown.test.tsx | 10 - .../Drilldown/__tests__/menuOptions.test.tsx | 48 + .../QueryTable/Drilldown/menuOptions.tsx | 11 +- .../Drilldown/useAggregateDrilldown.tsx | 15 - .../Drilldown/useBaseAggregateOptions.tsx | 12 - .../Drilldown/useDashboardVarConfig.tsx | 219 ---- .../useAddDynamicVariableToPanels.tsx | 49 - .../hooks/dashboard/useDeleteDashboard.tsx | 31 - .../hooks/dashboard/useGetAllDashboard.tsx | 39 - .../dashboard/useGetPublicDashboardData.tsx | 18 - .../dashboard/useGetPublicDashboardMeta.tsx | 20 - .../src/hooks/dashboard/useLockDashboard.ts | 40 - .../useWidgetsByDynamicVariableId.ts | 28 - .../lib/dashboardVariables/dependencyGraph.ts | 239 ++++ .../dashboardVariables/sortVariableValues.ts | 19 - .../src/pages/DashboardPage/DashboardPage.tsx | 53 - frontend/src/pages/DashboardPage/index.tsx | 7 - .../__tests__/variableCycleDetection.test.ts | 90 ++ .../Variables/utils/variableCycleDetection.ts | 25 +- .../DashboardsListPage.styles.scss | 30 - .../DashboardsListPage/DashboardsListPage.tsx | 34 - .../__tests__/DashboardListPage.test.tsx | 228 ---- .../src/pages/DashboardsListPage/index.tsx | 7 - .../Dashboard/__tests__/Dashboard.test.tsx | 743 ------------ .../helpers/panelTypeSelectionModalHelper.ts | 17 - .../dashboardVariablesStoreUtils.ts | 2 +- frontend/src/types/api/dashboard/create.ts | 12 - frontend/src/types/api/dashboard/delete.ts | 10 - .../src/types/api/dashboard/lockUnlock.ts | 11 - .../src/types/api/dashboard/public/create.ts | 5 - .../src/types/api/dashboard/public/delete.ts | 8 - .../src/types/api/dashboard/public/update.ts | 6 - 149 files changed, 407 insertions(+), 21382 deletions(-) delete mode 100644 frontend/src/api/dashboard/public/createPublicDashboard.ts delete mode 100644 frontend/src/api/dashboard/public/getPublicDashboardMeta.ts delete mode 100644 frontend/src/api/dashboard/public/revokePublicDashboardAccess.ts delete mode 100644 frontend/src/api/dashboard/public/updatePublicDashboard.ts delete mode 100644 frontend/src/api/v1/dashboards/create.ts delete mode 100644 frontend/src/api/v1/dashboards/getAll.ts delete mode 100644 frontend/src/api/v1/dashboards/id/delete.ts delete mode 100644 frontend/src/api/v1/dashboards/id/lock.ts delete mode 100644 frontend/src/assets/CustomIcons/ApacheIcon.tsx delete mode 100644 frontend/src/assets/CustomIcons/DockerIcon.tsx delete mode 100644 frontend/src/assets/CustomIcons/ElasticSearchIcon.tsx delete mode 100644 frontend/src/assets/CustomIcons/HerokuIcon.tsx delete mode 100644 frontend/src/assets/CustomIcons/KubernetesIcon.tsx delete mode 100644 frontend/src/assets/CustomIcons/MongoDBIcon.tsx delete mode 100644 frontend/src/assets/CustomIcons/MySQLIcon.tsx delete mode 100644 frontend/src/assets/CustomIcons/NginxIcon.tsx delete mode 100644 frontend/src/assets/CustomIcons/PostgreSQLIcon.tsx delete mode 100644 frontend/src/assets/CustomIcons/RedisIcon.tsx delete mode 100644 frontend/src/components/NewSelect/__test__/VariableItem.integration.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardDescription/Description.styles.scss delete mode 100644 frontend/src/container/DashboardContainer/DashboardDescription/SettingsDrawer/SettingsDrawer.styles.scss delete mode 100644 frontend/src/container/DashboardContainer/DashboardDescription/SettingsDrawer/index.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardDescription/__tests__/DashboardDescription.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardDescription/index.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardDescription/types.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardDescription/utils.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardSettings.styles.scss delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardSettingsContent.styles.scss delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/DynamicVariable/DynamicVariable.styles.scss delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/DynamicVariable/DynamicVariable.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/DynamicVariable/__test__/DynamicVariable.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/VariableItem.styles.scss delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/VariableItem.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/VariableItem.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/WidgetSelector.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/styles.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/addTagFiltersToDashboard.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/addTagFiltersToDashboard.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/index.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/types.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/General/AddBadges/AddBadges.module.scss delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/General/AddBadges/index.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/General/GeneralSettings.module.scss delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/General/index.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/General/styles.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/General/utils.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/PublicDashboard.styles.scss delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/__tests__/PublicDashboard.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/index.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardSettings/index.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/CustomVariableInput.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/DashboardVariableSelection.styles.scss delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/DashboardVariableSelection.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/DynamicVariableInput.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/QueryVariableInput.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/SelectVariableInput.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/TextboxVariableInput.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/VariableItem.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/VariableItem.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/DashboardVariableSelection.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/DynamicVariableInput.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/QueryVariableInput.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/VariableItem.defaulting.behavior.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/dashboardVariables.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/mock.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/index.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/styles.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/useDashboardVariableSelectHelper.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/useDashboardVariableUpdate.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/util.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/utils.test.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/customVariableSelectStrategy.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/defaultVariableSelectStrategy.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/dynamicVariableSelectStrategy.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/queryVariableSelectStrategy.tsx delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/variableSelectStrategyTypes.ts delete mode 100644 frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/variableSelectStrategyUtils.ts delete mode 100644 frontend/src/container/DashboardContainer/GridGraphs/index.tsx delete mode 100644 frontend/src/container/DashboardContainer/GridGraphs/styles.ts delete mode 100644 frontend/src/container/DashboardContainer/PanelTypeSelectionModal/PanelTypeSelectionModal.styles.scss delete mode 100644 frontend/src/container/DashboardContainer/PanelTypeSelectionModal/index.tsx delete mode 100644 frontend/src/container/DashboardContainer/__test__/DynamicVariableDefaultBehavior.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/__test__/IntegrationTests.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/__test__/PanelManagement.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/__test__/VariableItem.test.tsx delete mode 100644 frontend/src/container/DashboardContainer/components/DashboardHeader/DashboardBreadcrumbs.styles.scss delete mode 100644 frontend/src/container/DashboardContainer/components/DashboardHeader/DashboardBreadcrumbs.tsx delete mode 100644 frontend/src/container/DashboardContainer/components/DashboardHeader/DashboardHeader.styles.scss delete mode 100644 frontend/src/container/DashboardContainer/components/DashboardHeader/DashboardHeader.tsx delete mode 100644 frontend/src/container/DashboardContainer/index.tsx delete mode 100644 frontend/src/container/DashboardContainer/utils.ts delete mode 100644 frontend/src/container/GridCardLayout/DashboardEmptyState/DashboardEmptyState.styles.scss delete mode 100644 frontend/src/container/GridCardLayout/DashboardEmptyState/DashboardEmptyState.tsx delete mode 100644 frontend/src/container/GridCardLayout/GridCardLayout.tsx delete mode 100644 frontend/src/container/GridCardLayout/WidgetRow.tsx delete mode 100644 frontend/src/container/GridCardLayout/index.tsx rename frontend/src/container/{DashboardContainer/PanelTypeSelectionModal/menuItems.tsx => GridCardLayout/panelTypeItems.tsx} (100%) delete mode 100644 frontend/src/container/ListOfDashboard/DashboardActions.module.scss delete mode 100644 frontend/src/container/ListOfDashboard/DashboardList.styles.scss delete mode 100644 frontend/src/container/ListOfDashboard/DashboardTemplates/DashboardTemplatesContent.tsx delete mode 100644 frontend/src/container/ListOfDashboard/DashboardTemplates/DashboardTemplatesModal.styles.scss delete mode 100644 frontend/src/container/ListOfDashboard/DashboardTemplates/DashboardTemplatesModal.tsx delete mode 100644 frontend/src/container/ListOfDashboard/DashboardsList.tsx delete mode 100644 frontend/src/container/ListOfDashboard/ImportJSON/importJSON.styles.scss delete mode 100644 frontend/src/container/ListOfDashboard/ImportJSON/index.tsx delete mode 100644 frontend/src/container/ListOfDashboard/RequestDashboardBtn.tsx delete mode 100644 frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx delete mode 100644 frontend/src/container/ListOfDashboard/index.tsx delete mode 100644 frontend/src/container/ListOfDashboard/utils.ts create mode 100644 frontend/src/container/QueryTable/Drilldown/__tests__/menuOptions.test.tsx delete mode 100644 frontend/src/container/QueryTable/Drilldown/useDashboardVarConfig.tsx delete mode 100644 frontend/src/hooks/dashboard/useAddDynamicVariableToPanels.tsx delete mode 100644 frontend/src/hooks/dashboard/useDeleteDashboard.tsx delete mode 100644 frontend/src/hooks/dashboard/useGetAllDashboard.tsx delete mode 100644 frontend/src/hooks/dashboard/useGetPublicDashboardData.tsx delete mode 100644 frontend/src/hooks/dashboard/useGetPublicDashboardMeta.tsx delete mode 100644 frontend/src/hooks/dashboard/useLockDashboard.ts delete mode 100644 frontend/src/hooks/dashboard/useWidgetsByDynamicVariableId.ts create mode 100644 frontend/src/lib/dashboardVariables/dependencyGraph.ts delete mode 100644 frontend/src/lib/dashboardVariables/sortVariableValues.ts delete mode 100644 frontend/src/pages/DashboardPage/DashboardPage.tsx delete mode 100644 frontend/src/pages/DashboardPage/index.tsx create mode 100644 frontend/src/pages/DashboardPageV2/DashboardContainer/DashboardSettings/Variables/utils/__tests__/variableCycleDetection.test.ts delete mode 100644 frontend/src/pages/DashboardsListPage/DashboardsListPage.styles.scss delete mode 100644 frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx delete mode 100644 frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx delete mode 100644 frontend/src/pages/DashboardsListPage/index.tsx delete mode 100644 frontend/src/providers/Dashboard/__tests__/Dashboard.test.tsx delete mode 100644 frontend/src/providers/Dashboard/helpers/panelTypeSelectionModalHelper.ts delete mode 100644 frontend/src/types/api/dashboard/create.ts delete mode 100644 frontend/src/types/api/dashboard/delete.ts delete mode 100644 frontend/src/types/api/dashboard/lockUnlock.ts delete mode 100644 frontend/src/types/api/dashboard/public/create.ts delete mode 100644 frontend/src/types/api/dashboard/public/delete.ts delete mode 100644 frontend/src/types/api/dashboard/public/update.ts diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 0f1edd2b74b..d7d2a3a2e45 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -94,12 +94,12 @@ export const OnboardingV2 = Loadable( export const DashboardsListPage = Loadable( () => import( - /* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPage' + /* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPageV2' ), ); export const DashboardPage = Loadable( - () => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPage'), + () => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPageV2'), ); export const DashboardWidget = Loadable( diff --git a/frontend/src/api/dashboard/public/createPublicDashboard.ts b/frontend/src/api/dashboard/public/createPublicDashboard.ts deleted file mode 100644 index f03183ebd50..00000000000 --- a/frontend/src/api/dashboard/public/createPublicDashboard.ts +++ /dev/null @@ -1,36 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; -import { AxiosError } from 'axios'; -import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; -import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; -import { CreatePublicDashboardProps } from 'types/api/dashboard/public/create'; - -/** - * @deprecated Use the generated `useCreatePublicDashboard` hook (or `createPublicDashboard` fetcher) from - * `api/generated/services/dashboard` instead. This hand-written client targets the - * same endpoint and will be removed once call sites migrate. - * - * Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method. - */ -const createPublicDashboard = async ( - props: CreatePublicDashboardProps, -): Promise> => { - - const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props; - - try { - const response = await axios.post( - `/dashboards/${dashboardId}/public`, - { timeRangeEnabled, defaultTimeRange }, - ); - - return { - httpStatusCode: response.status, - data: response.data.data, - }; - } catch (error) { - ErrorResponseHandlerV2(error as AxiosError); - } -}; - -export default createPublicDashboard; diff --git a/frontend/src/api/dashboard/public/getPublicDashboardMeta.ts b/frontend/src/api/dashboard/public/getPublicDashboardMeta.ts deleted file mode 100644 index 72a4fe9b1d8..00000000000 --- a/frontend/src/api/dashboard/public/getPublicDashboardMeta.ts +++ /dev/null @@ -1,27 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; -import { AxiosError } from 'axios'; -import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; -import { GetPublicDashboardMetaProps, PayloadProps,PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta'; - -/** - * @deprecated Use the generated `useGetPublicDashboard` hook (or `getPublicDashboard` fetcher) from - * `api/generated/services/dashboard` instead. This hand-written client targets the - * same endpoint and will be removed once call sites migrate. - * - * Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method. - */ -const getPublicDashboardMeta = async (props: GetPublicDashboardMetaProps): Promise> => { - try { - const response = await axios.get(`/dashboards/${props.id}/public`); - - return { - httpStatusCode: response.status, - data: response.data.data, - }; - } catch (error) { - ErrorResponseHandlerV2(error as AxiosError); - } -}; - -export default getPublicDashboardMeta; diff --git a/frontend/src/api/dashboard/public/revokePublicDashboardAccess.ts b/frontend/src/api/dashboard/public/revokePublicDashboardAccess.ts deleted file mode 100644 index 155ceb72f9b..00000000000 --- a/frontend/src/api/dashboard/public/revokePublicDashboardAccess.ts +++ /dev/null @@ -1,29 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; -import { AxiosError } from 'axios'; -import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; -import { PayloadProps,RevokePublicDashboardAccessProps } from 'types/api/dashboard/public/delete'; - -/** - * @deprecated Use the generated `useDeletePublicDashboard` hook (or `deletePublicDashboard` fetcher) from - * `api/generated/services/dashboard` instead. This hand-written client targets the - * same endpoint and will be removed once call sites migrate. - * - * Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method. - */ -const revokePublicDashboardAccess = async ( - props: RevokePublicDashboardAccessProps, -): Promise> => { - try { - const response = await axios.delete(`/dashboards/${props.id}/public`); - - return { - httpStatusCode: response.status, - data: response.data, - }; - } catch (error) { - ErrorResponseHandlerV2(error as AxiosError); - } -}; - -export default revokePublicDashboardAccess; diff --git a/frontend/src/api/dashboard/public/updatePublicDashboard.ts b/frontend/src/api/dashboard/public/updatePublicDashboard.ts deleted file mode 100644 index 33b416da85f..00000000000 --- a/frontend/src/api/dashboard/public/updatePublicDashboard.ts +++ /dev/null @@ -1,36 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; -import { AxiosError } from 'axios'; -import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; -import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; -import { UpdatePublicDashboardProps } from 'types/api/dashboard/public/update'; - -/** - * @deprecated Use the generated `useUpdatePublicDashboard` hook (or `updatePublicDashboard` fetcher) from - * `api/generated/services/dashboard` instead. This hand-written client targets the - * same endpoint and will be removed once call sites migrate. - * - * Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method. - */ -const updatePublicDashboard = async ( - props: UpdatePublicDashboardProps, -): Promise> => { - - const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props; - - try { - const response = await axios.put( - `/dashboards/${dashboardId}/public`, - { timeRangeEnabled, defaultTimeRange }, - ); - - return { - httpStatusCode: response.status, - data: response.data.data, - }; - } catch (error) { - ErrorResponseHandlerV2(error as AxiosError); - } -}; - -export default updatePublicDashboard; diff --git a/frontend/src/api/v1/dashboards/create.ts b/frontend/src/api/v1/dashboards/create.ts deleted file mode 100644 index 1eb7bdc12e3..00000000000 --- a/frontend/src/api/v1/dashboards/create.ts +++ /dev/null @@ -1,23 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; -import { AxiosError } from 'axios'; -import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; -import { PayloadProps, Props } from 'types/api/dashboard/create'; -import { Dashboard } from 'types/api/dashboard/getAll'; - -const create = async (props: Props): Promise> => { - try { - const response = await axios.post('/dashboards', { - ...props, - }); - - return { - httpStatusCode: response.status, - data: response.data.data, - }; - } catch (error) { - ErrorResponseHandlerV2(error as AxiosError); - } -}; - -export default create; diff --git a/frontend/src/api/v1/dashboards/getAll.ts b/frontend/src/api/v1/dashboards/getAll.ts deleted file mode 100644 index 7d89ef94f03..00000000000 --- a/frontend/src/api/v1/dashboards/getAll.ts +++ /dev/null @@ -1,19 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; -import { AxiosError } from 'axios'; -import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; -import { Dashboard, PayloadProps } from 'types/api/dashboard/getAll'; - -const getAll = async (): Promise> => { - try { - const response = await axios.get('/dashboards'); - return { - httpStatusCode: response.status, - data: response.data.data, - }; - } catch (error) { - ErrorResponseHandlerV2(error as AxiosError); - } -}; - -export default getAll; diff --git a/frontend/src/api/v1/dashboards/id/delete.ts b/frontend/src/api/v1/dashboards/id/delete.ts deleted file mode 100644 index e6c974df026..00000000000 --- a/frontend/src/api/v1/dashboards/id/delete.ts +++ /dev/null @@ -1,21 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; -import { AxiosError } from 'axios'; -import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; -import { PayloadProps, Props } from 'types/api/dashboard/delete'; - -const deleteDashboard = async ( - props: Props, -): Promise> => { - try { - const response = await axios.delete(`/dashboards/${props.id}`); - return { - httpStatusCode: response.status, - data: null, - }; - } catch (error) { - ErrorResponseHandlerV2(error as AxiosError); - } -}; - -export default deleteDashboard; diff --git a/frontend/src/api/v1/dashboards/id/lock.ts b/frontend/src/api/v1/dashboards/id/lock.ts deleted file mode 100644 index 289a4ddc990..00000000000 --- a/frontend/src/api/v1/dashboards/id/lock.ts +++ /dev/null @@ -1,23 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; -import { AxiosError } from 'axios'; -import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; -import { PayloadProps, Props } from 'types/api/dashboard/lockUnlock'; - -const lock = async (props: Props): Promise> => { - try { - const response = await axios.put( - `/dashboards/${props.id}/lock`, - { lock: props.lock }, - ); - - return { - httpStatusCode: response.status, - data: response.data.data, - }; - } catch (error) { - ErrorResponseHandlerV2(error as AxiosError); - } -}; - -export default lock; diff --git a/frontend/src/assets/CustomIcons/ApacheIcon.tsx b/frontend/src/assets/CustomIcons/ApacheIcon.tsx deleted file mode 100644 index 42deba96bd2..00000000000 --- a/frontend/src/assets/CustomIcons/ApacheIcon.tsx +++ /dev/null @@ -1,176 +0,0 @@ -export default function ApacheIcon(): JSX.Element { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/frontend/src/assets/CustomIcons/DockerIcon.tsx b/frontend/src/assets/CustomIcons/DockerIcon.tsx deleted file mode 100644 index ef91bb7c9d2..00000000000 --- a/frontend/src/assets/CustomIcons/DockerIcon.tsx +++ /dev/null @@ -1,28 +0,0 @@ -export default function DockerIcon(): JSX.Element { - return ( - - - - - - - - - - - ); -} diff --git a/frontend/src/assets/CustomIcons/ElasticSearchIcon.tsx b/frontend/src/assets/CustomIcons/ElasticSearchIcon.tsx deleted file mode 100644 index d251b7d756a..00000000000 --- a/frontend/src/assets/CustomIcons/ElasticSearchIcon.tsx +++ /dev/null @@ -1,36 +0,0 @@ -export default function ElasticSearchIcon(): JSX.Element { - return ( - - - - - - - - - ); -} diff --git a/frontend/src/assets/CustomIcons/HerokuIcon.tsx b/frontend/src/assets/CustomIcons/HerokuIcon.tsx deleted file mode 100644 index 3d68fdb6e48..00000000000 --- a/frontend/src/assets/CustomIcons/HerokuIcon.tsx +++ /dev/null @@ -1,27 +0,0 @@ -function HerokuIcon(): JSX.Element { - return ( - - - - - - - - - - - ); -} - -export default HerokuIcon; diff --git a/frontend/src/assets/CustomIcons/KubernetesIcon.tsx b/frontend/src/assets/CustomIcons/KubernetesIcon.tsx deleted file mode 100644 index 280febebbe7..00000000000 --- a/frontend/src/assets/CustomIcons/KubernetesIcon.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export default function KubernetesIcon(): JSX.Element { - return ( - - - - - ); -} diff --git a/frontend/src/assets/CustomIcons/MongoDBIcon.tsx b/frontend/src/assets/CustomIcons/MongoDBIcon.tsx deleted file mode 100644 index d0bd3c46803..00000000000 --- a/frontend/src/assets/CustomIcons/MongoDBIcon.tsx +++ /dev/null @@ -1,68 +0,0 @@ -export default function MongoDBIcon(): JSX.Element { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/frontend/src/assets/CustomIcons/MySQLIcon.tsx b/frontend/src/assets/CustomIcons/MySQLIcon.tsx deleted file mode 100644 index 1cebec32ba5..00000000000 --- a/frontend/src/assets/CustomIcons/MySQLIcon.tsx +++ /dev/null @@ -1,28 +0,0 @@ -export default function MySQLIcon(): JSX.Element { - return ( - - - - - - - - - - - ); -} diff --git a/frontend/src/assets/CustomIcons/NginxIcon.tsx b/frontend/src/assets/CustomIcons/NginxIcon.tsx deleted file mode 100644 index 6e93057ab1b..00000000000 --- a/frontend/src/assets/CustomIcons/NginxIcon.tsx +++ /dev/null @@ -1,22 +0,0 @@ -function NginxIcon(): JSX.Element { - return ( - - - - - ); -} - -export default NginxIcon; diff --git a/frontend/src/assets/CustomIcons/PostgreSQLIcon.tsx b/frontend/src/assets/CustomIcons/PostgreSQLIcon.tsx deleted file mode 100644 index b3c2b1eed26..00000000000 --- a/frontend/src/assets/CustomIcons/PostgreSQLIcon.tsx +++ /dev/null @@ -1,40 +0,0 @@ -export default function PostgreSQLIcon(): JSX.Element { - return ( - - - - - - - - - - - - - - ); -} diff --git a/frontend/src/assets/CustomIcons/RedisIcon.tsx b/frontend/src/assets/CustomIcons/RedisIcon.tsx deleted file mode 100644 index 714eeb302f6..00000000000 --- a/frontend/src/assets/CustomIcons/RedisIcon.tsx +++ /dev/null @@ -1,60 +0,0 @@ -export default function RedisIcon(): JSX.Element { - return ( - - - - - - - - - - - - - - - - - - - ); -} diff --git a/frontend/src/components/NewSelect/__test__/VariableItem.integration.test.tsx b/frontend/src/components/NewSelect/__test__/VariableItem.integration.test.tsx deleted file mode 100644 index c1f4b5ea1ae..00000000000 --- a/frontend/src/components/NewSelect/__test__/VariableItem.integration.test.tsx +++ /dev/null @@ -1,585 +0,0 @@ -import { QueryClient, QueryClientProvider } from 'react-query'; -// eslint-disable-next-line no-restricted-imports -import { Provider } from 'react-redux'; -import { VirtuosoMockContext } from 'react-virtuoso'; -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import configureStore from 'redux-mock-store'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -import VariableItem from '../../../container/DashboardContainer/DashboardVariablesSelection/VariableItem'; - -// Mock the dashboard variables query -jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({ - __esModule: true, - default: jest.fn(() => - Promise.resolve({ - payload: { - variableValues: ['option1', 'option2', 'option3', 'option4'], - }, - }), - ), -})); - -// Mock scrollIntoView which isn't available in JSDOM -window.HTMLElement.prototype.scrollIntoView = jest.fn(); - -// Constants -const TEST_VARIABLE_NAME = 'test_variable'; -const TEST_VARIABLE_ID = 'test-var-id'; - -// Create a mock store -const mockStore = configureStore([])({ - globalTime: { - minTime: Date.now() - 3600000, // 1 hour ago - maxTime: Date.now(), - }, -}); - -// Test data -const createMockVariable = ( - overrides: Partial = {}, -): IDashboardVariable => ({ - id: TEST_VARIABLE_ID, - name: TEST_VARIABLE_NAME, - description: 'Test variable description', - type: 'QUERY', - queryValue: 'SELECT DISTINCT value FROM table', - customValue: '', - sort: 'ASC', - multiSelect: false, - showALLOption: true, - selectedValue: [], - allSelected: false, - ...overrides, -}); - -function TestWrapper({ children }: { children: React.ReactNode }): JSX.Element { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - }, - }); - - return ( - - - - {children} - - - - ); -} - -describe('VariableItem Integration Tests', () => { - let user: ReturnType; - let mockOnValueUpdate: jest.Mock; - - beforeEach(() => { - user = userEvent.setup(); - mockOnValueUpdate = jest.fn(); - jest.clearAllMocks(); - }); - - // ===== 1. INTEGRATION WITH CUSTOMSELECT ===== - describe('CustomSelect Integration (VI)', () => { - it('VI-01: Single select variable integration', async () => { - const variable = createMockVariable({ - multiSelect: false, - type: 'CUSTOM', - customValue: 'option1,option2,option3', - }); - - render( - - - , - ); - - // Should render with CustomSelect - const combobox = screen.getByRole('combobox'); - expect(combobox).toBeInTheDocument(); - - await user.click(combobox); - - await waitFor(() => { - expect(screen.getByText('option1')).toBeInTheDocument(); - expect(screen.getByText('option2')).toBeInTheDocument(); - expect(screen.getByText('option3')).toBeInTheDocument(); - }); - - // Select an option - const option1 = screen.getByText('option1'); - await user.click(option1); - - expect(mockOnValueUpdate).toHaveBeenCalledWith( - TEST_VARIABLE_NAME, - TEST_VARIABLE_ID, - 'option1', - false, - ); - }); - }); - - // ===== 2. INTEGRATION WITH CUSTOMMULTISELECT ===== - describe('CustomMultiSelect Integration (VI)', () => { - it('VI-02: Multi select variable integration', async () => { - const variable = createMockVariable({ - multiSelect: true, - type: 'CUSTOM', - customValue: 'option1,option2,option3,option4', - showALLOption: true, - }); - - render( - - - , - ); - - // Should render with CustomMultiSelect - const combobox = screen.getByRole('combobox'); - expect(combobox).toBeInTheDocument(); - - await user.click(combobox); - - await waitFor(() => { - // Should show ALL option - expect(screen.getByText('ALL')).toBeInTheDocument(); - }); - - // Wait for Virtuoso to render the custom options - await waitFor( - () => { - expect(screen.getByText('option1')).toBeInTheDocument(); - expect(screen.getByText('option2')).toBeInTheDocument(); - expect(screen.getByText('option3')).toBeInTheDocument(); - expect(screen.getByText('option4')).toBeInTheDocument(); - }, - { timeout: 5000 }, - ); - }); - }); - - // ===== 3. TEXTBOX VARIABLE TYPE ===== - describe('Textbox Variable Integration', () => { - it('VI-03: Textbox variable handling', async () => { - const variable = createMockVariable({ - type: 'TEXTBOX', - selectedValue: 'initial-value', - }); - - render( - - - , - ); - - // Should render a regular input - const textInput = screen.getByDisplayValue('initial-value'); - expect(textInput).toBeInTheDocument(); - expect(textInput.tagName).toBe('INPUT'); - - // Clear and type new value - await user.clear(textInput); - await user.type(textInput, 'new-text-value'); - - // Blur the input to trigger the value update - await user.tab(); - - // Should call onValueUpdate after blur - await waitFor( - () => { - expect(mockOnValueUpdate).toHaveBeenCalledWith( - TEST_VARIABLE_NAME, - TEST_VARIABLE_ID, - 'new-text-value', - false, - ); - }, - { timeout: 1000 }, - ); - }); - }); - - // ===== 4. VALUE PERSISTENCE AND STATE MANAGEMENT ===== - describe('Value Persistence and State Management', () => { - it('VI-04: All selected state handling', () => { - const variable = createMockVariable({ - multiSelect: true, - type: 'CUSTOM', - customValue: 'service1,service2,service3', - selectedValue: ['service1', 'service2', 'service3'], - allSelected: true, - showALLOption: true, - }); - - render( - - - , - ); - - // Should show "ALL" instead of individual values - expect(screen.getByText('ALL')).toBeInTheDocument(); - }); - - it('VI-05: Dropdown behavior with temporary selections', async () => { - const variable = createMockVariable({ - multiSelect: true, - type: 'CUSTOM', - customValue: 'item1,item2,item3', - selectedValue: ['item1'], - }); - - render( - - - , - ); - - const combobox = screen.getByRole('combobox'); - await user.click(combobox); - - // Wait for dropdown to open - await waitFor(() => { - const dropdown = document.querySelector('.ant-select-dropdown'); - expect(dropdown).toBeInTheDocument(); - }); - - // Verify the component renders without crashing - expect(combobox).toBeInTheDocument(); - }); - }); - - // ===== 6. ACCESSIBILITY AND USER EXPERIENCE ===== - describe('Accessibility and User Experience', () => { - it('VI-06: Variable description tooltip', async () => { - const variable = createMockVariable({ - description: 'This variable controls the service selection', - type: 'CUSTOM', - customValue: 'service1,service2', - }); - - render( - - - , - ); - - // Should show info icon - const infoIcon = document.querySelector('.info-icon'); - expect(infoIcon).toBeInTheDocument(); - - // Hover to show tooltip - if (infoIcon) { - await user.hover(infoIcon); - } - - await waitFor(() => { - expect( - screen.getByText('This variable controls the service selection'), - ).toBeInTheDocument(); - }); - }); - - it('VI-07: Variable name display', () => { - const variable = createMockVariable({ - name: 'service_name', - type: 'CUSTOM', - customValue: 'service1,service2', - }); - - render( - - - , - ); - - // Should show variable name with $ prefix - expect(screen.getByText('$service_name')).toBeInTheDocument(); - }); - - it('VI-08: Max tag count behavior', async () => { - const variable = createMockVariable({ - multiSelect: true, - type: 'CUSTOM', - customValue: 'tag1,tag2,tag3,tag4,tag5,tag6,tag7', - selectedValue: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'], - }); - - render( - - - , - ); - - // Wait for component to render - await waitFor(() => { - const combobox = screen.getByRole('combobox'); - expect(combobox).toBeInTheDocument(); - }); - - // Should show limited number of tags with "+ X more" - const tags = document.querySelectorAll('.ant-select-selection-item'); - - // The component should render without crashing - expect(tags.length).toBeGreaterThanOrEqual(0); - }); - }); - - // ===== 8. SEARCH INTERACTION TESTS ===== - describe('Search Interaction Tests', () => { - it('VI-14: Search persistence across dropdown open/close', async () => { - const variable = createMockVariable({ - type: 'CUSTOM', - customValue: 'option1,option2,option3', - multiSelect: true, - }); - - render( - - - , - ); - - const combobox = screen.getByRole('combobox'); - await user.click(combobox); - - await waitFor(() => { - expect(screen.getByText('ALL')).toBeInTheDocument(); - }); - - const searchInput = document.querySelector( - '.ant-select-selection-search-input', - ); - expect(searchInput).toBeInTheDocument(); - - if (searchInput) { - await user.type(searchInput, 'search-text'); - } - - // Verify search text is in input - await waitFor(() => { - expect(searchInput).toHaveValue('search-text'); - }); - - // Press Escape to close dropdown - await user.keyboard('{Escape}'); - - // Dropdown should close and search text should be cleared - await waitFor(() => { - const dropdown = document.querySelector('.ant-select-dropdown'); - expect(dropdown).toHaveClass('ant-select-dropdown-hidden'); - expect(searchInput).toHaveValue(''); - }); - }); - }); - - // ===== 9. ADVANCED KEYBOARD NAVIGATION ===== - describe('Advanced Keyboard Navigation (VI)', () => { - it('VI-15: Shift + Arrow + Del chip deletion in multiselect', async () => { - const variable = createMockVariable({ - type: 'CUSTOM', - customValue: 'option1,option2,option3', - multiSelect: true, - selectedValue: ['option1', 'option2', 'option3'], - }); - - render( - - - , - ); - - const combobox = screen.getByRole('combobox'); - await user.click(combobox); - - // Navigate to chips using arrow keys - await user.keyboard('{ArrowLeft}'); - - // Use Shift + Arrow to navigate between chips - await user.keyboard('{Shift>}{ArrowLeft}{/Shift}'); - - // Use Del to delete the active chip - await user.keyboard('{Delete}'); - - // Note: The component may not immediately call onValueUpdate - // This test verifies the chip deletion behavior - await waitFor(() => { - // Check if a chip was removed from the selection - const selectionItems = document.querySelectorAll( - '.ant-select-selection-item', - ); - expect(selectionItems.length).toBeLessThan(3); - }); - }); - }); - - // ===== 11. ADVANCED UI STATES ===== - describe('Advanced UI States (VI)', () => { - it('VI-19: No data with previous value selected in variable', async () => { - const variable = createMockVariable({ - type: 'CUSTOM', - customValue: '', - multiSelect: true, - selectedValue: ['previous-value'], - }); - - render( - - - , - ); - - // Wait for component to initialize - await waitFor(() => { - const combobox = screen.getByRole('combobox'); - expect(combobox).toBeInTheDocument(); - }); - - const combobox = screen.getByRole('combobox'); - await user.click(combobox); - - // Should show no data message (the component may not show this exact text) - await waitFor(() => { - // Check if dropdown is empty or shows no data indication - const dropdown = document.querySelector('.ant-select-dropdown'); - expect(dropdown).toBeInTheDocument(); - }); - - // Verify the component renders without crashing - expect(combobox).toBeInTheDocument(); - }); - - it('VI-20: Always editable accessibility in variable', async () => { - const variable = createMockVariable({ - type: 'CUSTOM', - customValue: 'option1,option2', - multiSelect: true, - }); - - render( - - - , - ); - - const combobox = screen.getByRole('combobox'); - - // Should be editable - expect(combobox).not.toBeDisabled(); - await user.click(combobox); - expect(combobox).toHaveFocus(); - - // Should still be interactive - expect(combobox).not.toBeDisabled(); - }); - }); - - // ===== 13. DROPDOWN PERSISTENCE ===== - describe('Dropdown Persistence (VI)', () => { - it('VI-24: Dropdown stays open for non-save actions in variable', async () => { - const variable = createMockVariable({ - type: 'CUSTOM', - customValue: 'option1,option2,option3', - multiSelect: true, - }); - - render( - - - , - ); - - // Wait for component to initialize - await waitFor(() => { - const combobox = screen.getByRole('combobox'); - expect(combobox).toBeInTheDocument(); - }); - - const combobox = screen.getByRole('combobox'); - await user.click(combobox); - - await waitFor(() => { - expect(screen.getByText('ALL')).toBeInTheDocument(); - }); - - // Navigate with arrow keys (non-save action) - await user.keyboard('{ArrowDown}'); - await user.keyboard('{ArrowDown}'); - - // Dropdown should still be open - await waitFor(() => { - const dropdown = document.querySelector('.ant-select-dropdown'); - expect(dropdown).toBeInTheDocument(); - }); - - // Verify the component renders without crashing - expect(combobox).toBeInTheDocument(); - - // Only ESC should close the dropdown - await user.keyboard('{Escape}'); - - await waitFor(() => { - const dropdown = document.querySelector('.ant-select-dropdown'); - expect(dropdown).toHaveClass('ant-select-dropdown-hidden'); - }); - }); - }); -}); diff --git a/frontend/src/components/NewSelect/utils.ts b/frontend/src/components/NewSelect/utils.ts index 57fa054c784..02bfc3c1659 100644 --- a/frontend/src/components/NewSelect/utils.ts +++ b/frontend/src/components/NewSelect/utils.ts @@ -1,11 +1,26 @@ -import { uniqueOptions } from 'container/DashboardContainer/DashboardVariablesSelection/util'; - import { OptionData } from './types'; export const SPACEKEY = ' '; export const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value +/** De-duplicate options by `value`, keeping the first occurrence. */ +export const uniqueOptions = (options: OptionData[]): OptionData[] => { + const deduped: OptionData[] = []; + const seenValues = new Set(); + + options.forEach((option) => { + const value = option.value || ''; + if (seenValues.has(value)) { + return; + } + seenValues.add(value); + deduped.push(option); + }); + + return deduped; +}; + export const prioritizeOrAddOptionForSingleSelect = ( options: OptionData[], value: string, diff --git a/frontend/src/container/DashboardContainer/DashboardDescription/Description.styles.scss b/frontend/src/container/DashboardContainer/DashboardDescription/Description.styles.scss deleted file mode 100644 index b0ff7c5e4de..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardDescription/Description.styles.scss +++ /dev/null @@ -1,440 +0,0 @@ -.dashboard-description-container { - box-shadow: none; - border: none; - background: unset; - box-shadow: none; - color: var(--l2-foreground); - - .ant-card-body { - padding: 0px; - } - - .dashboard-details { - display: flex; - justify-content: space-between; - gap: 8px; - padding: 16px 16px 0px 16px; - align-items: flex-start; - - .left-section { - display: flex; - align-items: center; - gap: 8px; - width: 45%; - - .dashboard-img { - height: 16px; - width: 16px; - } - - .dashboard-title { - color: var(--l1-foreground); - font-family: Inter; - font-size: 16px; - font-style: normal; - font-weight: 500; - line-height: 24px; /* 150% */ - letter-spacing: -0.08px; - max-width: 80%; - - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; - } - - .public-dashboard-icon { - margin-right: 4px; - } - } - - .right-section { - display: flex; - width: 55%; - justify-content: flex-end; - flex-wrap: wrap; - align-items: center; - gap: 14px; - - .icons { - display: flex; - align-items: center; - width: 32px; - height: 34px; - padding: 6px; - justify-content: center; - border-radius: 2px; - border: 1px solid var(--l1-border); - background: var(--l3-background); - color: var(--l2-foreground); - 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(--l1-border); - background: var(--l3-background); - color: var(--l2-foreground); - 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(--primary-foreground); - background: var(--primary-background); - 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; - flex-wrap: wrap; - - .tag { - display: flex; - padding: 4px 8px; - justify-content: center; - align-items: center; - border-radius: 20px; - border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent); - background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent); - 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 { - color: var(--l2-foreground); - 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; - /* Preserve author-entered line breaks in the description. */ - white-space: pre-wrap; - overflow-wrap: anywhere; - - a { - color: var(--accent-primary); - text-decoration: underline; - - &:hover { - text-decoration: none; - } - } - } - - .dashboard-variables { - padding: 16px 16px 18px 16px; - } -} - -.dashboard-description { - display: -webkit-box; - -webkit-line-clamp: 2; /* Show up to 2 lines */ - -webkit-box-orient: vertical; - text-overflow: ellipsis; - overflow: hidden; -} - -.dashboard-settings { - width: 191px; - height: 302px; - flex-shrink: 0; - - .ant-popover-inner { - padding: 0px; - border-radius: 4px; - border: 1px solid var(--l1-border); - background: linear-gradient( - 139deg, - color-mix(in srgb, var(--card) 80%, transparent) 0%, - color-mix(in srgb, var(--card) 90%, transparent) 98.68% - ) !important; - box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); - backdrop-filter: blur(20px); - } - - .menu-content { - display: flex; - flex-direction: column; - - section { - display: flex; - flex-direction: column; - align-items: start; - - .ant-btn { - display: flex; - width: 100%; - height: unset; - padding: 8px; - align-items: center; - gap: 12px; - color: var(--l2-foreground); - font-family: Inter; - font-size: 13px; - font-style: normal; - font-weight: 400; - line-height: normal; - letter-spacing: 0.14px; - border-top: none; - - .ant-btn-icon { - margin-inline-end: 0px; - } - } - } - - .section-1, - .section-2 { - border-bottom: 1px solid var(--l1-border); - } - - .delete-dashboard .ant-btn { - color: var(--bg-cherry-400) !important; - } - } -} - -.rename-dashboard { - .ant-modal-content { - width: 384px; - flex-shrink: 0; - border-radius: 4px; - border: 1px solid var(--l1-border); - background: var(--l2-background); - box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); - padding: 0px; - - .ant-modal-header { - height: 52px; - padding: 16px; - background: var(--l2-background); - border-bottom: 1px solid var(--l1-border); - margin-bottom: 0px; - .ant-modal-title { - color: var(--l1-foreground); - 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(--l1-foreground); - 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(--l1-border); - background: var(--l3-background); - } - } - } - - .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(--l1-border); - - .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(--primary-background); - - .ant-btn-icon { - margin-inline-end: 0px; - } - } - } - } - } -} - -.section-naming { - .ant-modal-content { - width: 384px; - flex-shrink: 0; - border-radius: 4px; - border: 1px solid var(--l1-border); - background: var(--l2-background); - box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); - padding: 0px; - - .ant-modal-header { - height: 52px; - padding: 16px; - background: var(--l2-background); - border-bottom: 1px solid var(--l1-border); - margin-bottom: 0px; - .ant-modal-title { - color: var(--l1-foreground); - 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(--l1-foreground); - 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(--l1-border); - background: var(--l3-background); - } - } - } - - .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(--l1-border); - - .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(--primary-background); - - .ant-btn-icon { - margin-inline-end: 0px; - } - } - } - } - } -} diff --git a/frontend/src/container/DashboardContainer/DashboardDescription/SettingsDrawer/SettingsDrawer.styles.scss b/frontend/src/container/DashboardContainer/DashboardDescription/SettingsDrawer/SettingsDrawer.styles.scss deleted file mode 100644 index c8a7201d13c..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardDescription/SettingsDrawer/SettingsDrawer.styles.scss +++ /dev/null @@ -1,43 +0,0 @@ -.settings-container-root { - .ant-drawer-wrapper-body { - border-left: 1px solid var(--l1-border); - background: var(--l2-background); - box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2); - - .ant-drawer-header { - height: 48px; - border-bottom: 1px solid var(--l1-border); - padding: 14px 14px 14px 11px; - - .ant-drawer-header-title { - gap: 16px; - - .ant-drawer-title { - color: var(--l2-foreground); - 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 var(--l1-border); - } - - .ant-drawer-close { - height: 16px; - width: 16px; - margin-inline-end: 0px !important; - } - } - } - - .ant-drawer-body { - padding: 16px; - - &::-webkit-scrollbar { - width: 0.1rem; - } - } - } -} diff --git a/frontend/src/container/DashboardContainer/DashboardDescription/SettingsDrawer/index.tsx b/frontend/src/container/DashboardContainer/DashboardDescription/SettingsDrawer/index.tsx deleted file mode 100644 index a46b2b85110..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardDescription/SettingsDrawer/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { memo, PropsWithChildren, ReactElement } from 'react'; -import { Drawer } from 'antd'; -import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; - -import './SettingsDrawer.styles.scss'; - -type SettingsDrawerProps = PropsWithChildren<{ - drawerTitle: string; - isOpen: boolean; - onClose: () => void; -}>; - -function SettingsDrawer({ - children, - drawerTitle, - isOpen, - onClose, -}: SettingsDrawerProps): JSX.Element { - return ( - - {/* Need to type cast because of OverlayScrollbar type definition. We should be good once we remove it. */} - {children as ReactElement} - - ); -} - -export default memo(SettingsDrawer); diff --git a/frontend/src/container/DashboardContainer/DashboardDescription/__tests__/DashboardDescription.test.tsx b/frontend/src/container/DashboardContainer/DashboardDescription/__tests__/DashboardDescription.test.tsx deleted file mode 100644 index ed9bb335018..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardDescription/__tests__/DashboardDescription.test.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { ReactNode } from 'react'; -import { MemoryRouter, useLocation } from 'react-router-dom'; -import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap'; -import { - getDashboardById, - getNonIntegrationDashboardById, -} from 'mocks-server/__mockdata__/dashboards'; -import { server } from 'mocks-server/server'; -import { rest } from 'msw'; -import { - resetDashboard, - useDashboardStore, -} from 'providers/Dashboard/store/useDashboardStore'; -import { - fireEvent, - render, - screen, - userEvent, - waitFor, -} from 'tests/test-utils'; -import { Dashboard } from 'types/api/dashboard/getAll'; - -import DashboardDescription from '..'; - -function DashboardBootstrapWrapper({ - dashboardId, - children, -}: { - dashboardId: string; - children: ReactNode; -}): JSX.Element { - useDashboardBootstrap(dashboardId); - // eslint-disable-next-line react/jsx-no-useless-fragment - return <>{children}; -} - -interface MockSafeNavigateReturn { - safeNavigate: jest.MockedFunction<(url: string) => void>; -} - -const DASHBOARD_TEST_ID = 'dashboard-title'; -const DASHBOARD_TITLE_TEXT = 'thor'; -const DASHBOARD_PATH = '/dashboard/4'; - -const mockSafeNavigate = jest.fn(); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useLocation: jest.fn(), -})); - -jest.mock( - 'container/TopNav/DateTimeSelectionV2/index.tsx', - () => - function MockDateTimeSelection(): JSX.Element { - return
MockDateTimeSelection
; - }, -); - -jest.mock('hooks/useSafeNavigate', () => ({ - useSafeNavigate: (): MockSafeNavigateReturn => ({ - safeNavigate: mockSafeNavigate, - }), -})); - -describe('Dashboard landing page actions header tests', () => { - beforeEach(() => { - mockSafeNavigate.mockClear(); - sessionStorage.clear(); - resetDashboard(); - }); - - it('unlock dashboard should be disabled for integrations created dashboards', async () => { - const mockLocation = { - pathname: `${process.env.FRONTEND_API_ENDPOINT}${DASHBOARD_PATH}`, - search: '', - }; - (useLocation as jest.Mock).mockReturnValue(mockLocation); - const { getByTestId } = render( - - - => Promise.resolve(), - exit: (): Promise => Promise.resolve(), - node: { current: null }, - }} - /> - - , - ); - - await waitFor(() => - expect(getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent( - DASHBOARD_TITLE_TEXT, - ), - ); - - const dashboardSettingsTrigger = getByTestId('options'); - - await fireEvent.click(dashboardSettingsTrigger); - - const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard'); - - await waitFor(() => expect(lockUnlockButton).toBeDisabled()); - }); - - it('unlock dashboard should not be disabled for non integration created dashboards', async () => { - const mockLocation = { - pathname: `${process.env.FRONTEND_API_ENDPOINT}${DASHBOARD_PATH}`, - search: '', - }; - (useLocation as jest.Mock).mockReturnValue(mockLocation); - server.use( - rest.get('http://localhost/api/v1/dashboards/4', (_, res, ctx) => - res(ctx.status(200), ctx.json(getNonIntegrationDashboardById)), - ), - ); - const { getByTestId } = render( - - - => Promise.resolve(), - exit: (): Promise => Promise.resolve(), - node: { current: null }, - }} - /> - - , - ); - - await waitFor(() => - expect(getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent( - DASHBOARD_TITLE_TEXT, - ), - ); - - const dashboardSettingsTrigger = getByTestId('options'); - - await fireEvent.click(dashboardSettingsTrigger); - - const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard'); - - await waitFor(() => expect(lockUnlockButton).not.toBeDisabled()); - }); - - it('should navigate to base dashboard list URL when no saved params exist', async () => { - const user = userEvent.setup(); - const mockLocation = { - pathname: DASHBOARD_PATH, - search: '', - }; - - (useLocation as jest.Mock).mockReturnValue(mockLocation); - - const { getByText } = render( - - - => Promise.resolve(), - exit: (): Promise => Promise.resolve(), - node: { current: null }, - }} - /> - - , - ); - - await waitFor(() => - expect(screen.getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent( - DASHBOARD_TITLE_TEXT, - ), - ); - - const dashboardButton = getByText('Dashboard /'); - await user.click(dashboardButton); - - expect(mockSafeNavigate).toHaveBeenCalledWith('/dashboard'); - }); - - it('should navigate to dashboard list with saved query params when present', async () => { - const user = userEvent.setup(); - const savedParams = 'columnKey=createdAt&order=ascend&page=2&search=foo'; - sessionStorage.setItem('dashboardsListQueryParams', savedParams); - - const mockLocation = { - pathname: DASHBOARD_PATH, - search: '', - }; - - (useLocation as jest.Mock).mockReturnValue(mockLocation); - - useDashboardStore.setState({ - dashboardData: getDashboardById.data as unknown as Dashboard, - layouts: [], - panelMap: {}, - setPanelMap: jest.fn(), - setLayouts: jest.fn(), - setDashboardData: jest.fn(), - columnWidths: {}, - }); - - const { getByText } = render( - - => Promise.resolve(), - exit: (): Promise => Promise.resolve(), - node: { current: null }, - }} - /> - , - ); - - await waitFor(() => - expect(screen.getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent( - DASHBOARD_TITLE_TEXT, - ), - ); - - const dashboardButton = getByText('Dashboard /'); - await user.click(dashboardButton); - - expect(mockSafeNavigate).toHaveBeenCalledWith({ - pathname: '/dashboard', - search: `?${savedParams}`, - }); - }); -}); diff --git a/frontend/src/container/DashboardContainer/DashboardDescription/index.tsx b/frontend/src/container/DashboardContainer/DashboardDescription/index.tsx deleted file mode 100644 index b470771dc6d..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardDescription/index.tsx +++ /dev/null @@ -1,621 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { FullScreenHandle } from 'react-full-screen'; -import { Layout } from 'react-grid-layout'; -import { useTranslation } from 'react-i18next'; -import { useCopyToClipboard } from 'react-use'; -import { - Check, - ClipboardCopy, - Ellipsis, - FileJson, - FolderKanban, - Fullscreen, - Globe, - LockKeyhole, - PenLine, - Plus, - X, -} from '@signozhq/icons'; -import { Button, Card, Modal, Popover, Tooltip } from 'antd'; -import { Badge } from '@signozhq/ui/badge'; -import { Typography } from '@signozhq/ui/typography'; -import { Input } from '@signozhq/ui/input'; -import logEvent from 'api/common/logEvent'; -import ConfigureIcon from 'assets/Integrations/ConfigureIcon'; -import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; -import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton'; -import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; -import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables'; -import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta'; -import { useLockDashboard } from 'hooks/dashboard/useLockDashboard'; -import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; -import useComponentPermission from 'hooks/useComponentPermission'; -import { useGetTenantLicense } from 'hooks/useGetTenantLicense'; -import { useNotifications } from 'hooks/useNotifications'; -import { isEmpty } from 'lodash-es'; -import { useAppContext } from 'providers/App/App'; -import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper'; -import { - selectIsDashboardLocked, - useDashboardStore, -} from 'providers/Dashboard/store/useDashboardStore'; -import { sortLayout } from 'providers/Dashboard/util'; -import { DashboardData } from 'types/api/dashboard/getAll'; -import { Props } from 'types/api/dashboard/update'; -import { ROLES, USER_ROLES } from 'types/roles'; -import { linkifyText } from 'utils/linkifyText'; -import { ComponentTypes } from 'utils/permission'; -import { v4 as uuid } from 'uuid'; - -import DashboardHeader from '../components/DashboardHeader/DashboardHeader'; -import DashboardSettings from '../DashboardSettings'; -import { Base64Icons } from '../DashboardSettings/General/utils'; -import DashboardVariableSelection from '../DashboardVariablesSelection'; -import PanelTypeSelectionModal from '../PanelTypeSelectionModal'; -import SettingsDrawer from './SettingsDrawer'; -import { VariablesSettingsTab } from './types'; -import { - DEFAULT_ROW_NAME, - downloadObjectAsJson, - sanitizeDashboardData, -} from './utils'; - -import './Description.styles.scss'; - -interface DashboardDescriptionProps { - handle: FullScreenHandle; -} - -// eslint-disable-next-line sonarjs/cognitive-complexity -function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { - const { handle } = props; - const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore( - (s) => s.setIsPanelTypeSelectionModalOpen, - ); - const { - dashboardData, - panelMap, - setPanelMap, - layouts, - setLayouts, - setDashboardData, - } = useDashboardStore(); - - const isDashboardLocked = useDashboardStore(selectIsDashboardLocked); - const handleDashboardLockToggle = useLockDashboard(); - - const variablesSettingsTabHandle = useRef(null); - const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = - useState(false); - - const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense(); - - const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser; - - const selectedData = dashboardData - ? { - ...dashboardData.data, - uuid: dashboardData.id, - } - : ({} as DashboardData); - const { dashboardVariables } = useDashboardVariables(); - - const { - title = '', - description, - tags, - image = Base64Icons[0], - } = selectedData || {}; - - const [updatedTitle, setUpdatedTitle] = useState(title); - - const [sectionName, setSectionName] = useState(DEFAULT_ROW_NAME); - - const updateDashboardMutation = useUpdateDashboard(); - - const { user } = useAppContext(); - const [editDashboard] = useComponentPermission(['edit_dashboard'], user.role); - const [isDashboardSettingsOpen, setIsDashbordSettingsOpen] = - useState(false); - - const [isRenameDashboardOpen, setIsRenameDashboardOpen] = - useState(false); - - const [isPanelNameModalOpen, setIsPanelNameModalOpen] = - useState(false); - - const [isPublicDashboard, setIsPublicDashboard] = useState(false); - - let isAuthor = false; - - if (dashboardData && user && user.email) { - isAuthor = dashboardData?.createdBy === user?.email; - } - - let permissions: ComponentTypes[] = ['add_panel']; - - if (isDashboardLocked) { - permissions = ['add_panel_locked_dashboard']; - } - - const { notifications } = useNotifications(); - - const userRole: ROLES | null = - dashboardData?.createdBy === user?.email - ? (USER_ROLES.AUTHOR as ROLES) - : user.role; - - const [addPanelPermission] = useComponentPermission(permissions, userRole); - - const onEmptyWidgetHandler = useCallback(() => { - setIsPanelTypeSelectionModalOpen(true); - logEvent('Dashboard Detail: Add new panel clicked', { - dashboardId: dashboardData?.id, - dashboardName: dashboardData?.data.title, - numberOfPanels: dashboardData?.data.widgets?.length, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setIsPanelTypeSelectionModalOpen]); - - const handleLockDashboardToggle = (): void => { - setIsDashbordSettingsOpen(false); - handleDashboardLockToggle(!isDashboardLocked); - }; - - const onNameChangeHandler = (): void => { - if (!dashboardData) { - return; - } - const updatedDashboard: Props = { - id: dashboardData.id, - - data: { - ...dashboardData.data, - title: updatedTitle, - }, - }; - updateDashboardMutation.mutate(updatedDashboard, { - onSuccess: (updatedDashboard) => { - notifications.success({ - message: 'Dashboard renamed successfully', - }); - setIsRenameDashboardOpen(false); - if (updatedDashboard.data) { - setDashboardData(updatedDashboard.data); - } - }, - onError: () => { - setIsRenameDashboardOpen(true); - }, - }); - }; - - const [state, setCopy] = useCopyToClipboard(); - - const { t } = useTranslation(['dashboard', 'common']); - - // used to set the initial value for the updatedTitle - // the context value is sometimes not available during the initial render - // due to which the updatedTitle is set to some previous value - useEffect(() => { - if (dashboardData) { - setUpdatedTitle(dashboardData.data.title); - } - }, [dashboardData]); - - 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 (!dashboardData) { - 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: Props = { - id: dashboardData.id, - - data: { - ...dashboardData.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: [ - ...(dashboardData.data.widgets || []), - { - id, - title: sectionName, - description: '', - panelTypes: PANEL_GROUP_TYPES.ROW, - }, - ], - }, - }; - - updateDashboardMutation.mutate(updatedDashboard, { - onSuccess: (updatedDashboard) => { - if (updatedDashboard.data) { - if (updatedDashboard.data.data.layout) { - setLayouts(sortLayout(updatedDashboard.data.data.layout)); - } - setDashboardData(updatedDashboard.data); - setPanelMap(updatedDashboard.data?.data?.panelMap || {}); - } - - setIsPanelNameModalOpen(false); - setSectionName(DEFAULT_ROW_NAME); - }, - }); - } - - const { - data: publicDashboardResponse, - isLoading: isLoadingPublicDashboardData, - isFetching: isFetchingPublicDashboardData, - error: errorPublicDashboardData, - isError: isErrorPublicDashboardData, - } = useGetPublicDashboardMeta( - dashboardData?.id || '', - !!dashboardData?.id && isPublicDashboardEnabled, - ); - - useEffect(() => { - if (!isLoadingPublicDashboardData && !isFetchingPublicDashboardData) { - if (isErrorPublicDashboardData) { - const errorDetails = errorPublicDashboardData?.getErrorDetails(); - - if (errorDetails?.error?.code === 'public_dashboard_not_found') { - setIsPublicDashboard(false); - } - } else { - const publicDashboardData = publicDashboardResponse?.data; - if (publicDashboardData?.publicPath) { - setIsPublicDashboard(true); - } - } - } - }, [ - isLoadingPublicDashboardData, - isFetchingPublicDashboardData, - isErrorPublicDashboardData, - errorPublicDashboardData, - publicDashboardResponse?.data, - ]); - - const onConfigureClick = useCallback((): void => { - setIsSettingsDrawerOpen(true); - }, []); - - const onSettingsDrawerClose = useCallback((): void => { - setIsSettingsDrawerOpen(false); - // good use case for a state library like Jotai - if (variablesSettingsTabHandle.current) { - variablesSettingsTabHandle.current.resetState(); - } - }, []); - - return ( - - -
-
- dashboard-img - 30 ? title : ''}> - - {title} - - - - {isPublicDashboard && ( - - - - )} - - {isDashboardLocked && ( - - - - )} -
-
- - setIsDashbordSettingsOpen(visible)} - rootClassName="dashboard-settings" - content={ -
-
- {(isAuthor || user.role === USER_ROLES.ADMIN) && ( - - - - )} - - {!isDashboardLocked && editDashboard && ( - - )} - - -
-
- {!isDashboardLocked && addPanelPermission && ( - - )} - - - -
-
- -
-
- } - trigger="click" - placement="bottomRight" - > - - - - - - )} - {!isDashboardLocked && addPanelPermission && ( - - )} -
-
- {(tags?.length || 0) > 0 && ( -
- {tags?.map((tag) => ( - - {tag} - - ))} -
- )} - {!isEmpty(description) && ( -
- {linkifyText(description ?? '')} -
- )} - - {!isEmpty(dashboardVariables) && ( -
- -
- )} - - - { - // 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)} - /> -
-
-
- ); -} - -export default DashboardDescription; diff --git a/frontend/src/container/DashboardContainer/DashboardDescription/types.ts b/frontend/src/container/DashboardContainer/DashboardDescription/types.ts deleted file mode 100644 index 7a5cfcd5925..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardDescription/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MutableRefObject } from 'react'; - -export interface VariablesSettingsTab { - resetState: () => void; -} - -export type VariablesSettingsTabHandle = - MutableRefObject; diff --git a/frontend/src/container/DashboardContainer/DashboardDescription/utils.ts b/frontend/src/container/DashboardContainer/DashboardDescription/utils.ts deleted file mode 100644 index 54eee098fac..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardDescription/utils.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll'; - -export function sanitizeDashboardData( - selectedData: DashboardData, -): DashboardData { - if (!selectedData?.variables) { - return selectedData; - } - - const updatedVariables = Object.entries(selectedData.variables).reduce( - (acc, [key, value]) => { - const { selectedValue: _selectedValue, ...rest } = value; - acc[key] = rest; - return acc; - }, - {} as Record, - ); - - return { - ...selectedData, - variables: updatedVariables, - }; -} - -export function downloadObjectAsJson( - exportObj: unknown, - exportName: string, -): void { - const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent( - JSON.stringify(exportObj), - )}`; - const downloadAnchorNode = document.createElement('a'); - downloadAnchorNode.setAttribute('href', dataStr); - downloadAnchorNode.setAttribute('download', `${exportName}.json`); - document.body.appendChild(downloadAnchorNode); // required for firefox - downloadAnchorNode.click(); - downloadAnchorNode.remove(); -} - -export const DEFAULT_ROW_NAME = 'Sample Row'; diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardSettings.styles.scss b/frontend/src/container/DashboardContainer/DashboardSettings/DashboardSettings.styles.scss deleted file mode 100644 index 3f3a562149a..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardSettings.styles.scss +++ /dev/null @@ -1,123 +0,0 @@ -.delete-variable-name { - font-weight: 700; - color: rgb(207, 19, 34); - font-style: italic; -} - -.apply-to-all-variable-name { - font-weight: 700; - color: var(--bg-robin-400); - 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(--l1-border); - } - - .delete-variable-button { - width: 26px; - height: 22px; - border-radius: 2px; - background: color-mix(in srgb, var(--danger-background) 10%, transparent); - display: flex; - padding: 4px 6px; - align-items: center; - gap: 3px; - color: red; - } - - .apply-to-all-button { - width: min-content; - height: 22px; - border-radius: 2px; - display: flex; - padding: 0px 6px; - align-items: center; - gap: 3px; - background: var(--l1-border); - } - } - } - - .ant-table-cell-row-hover { - .actions-btns { - display: inline-flex; - } - } - - .ant-table-thead { - .ant-table-cell { - padding: 8px 12px; - border: 1px solid var(--l1-border); - background: unset; - } - - .ant-table-cell::before { - display: none; - } - } - - .ant-table-tbody { - .ant-table-cell { - padding: 14px; - border: 1px solid var(--l1-border); - } - } - - .ant-table-row { - .ant-table-cell:nth-child(even) { - background: color-mix(in srgb, var(--l1-border) 40%, transparent); - } - } -} diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardSettingsContent.styles.scss b/frontend/src/container/DashboardContainer/DashboardSettings/DashboardSettingsContent.styles.scss deleted file mode 100644 index fb50918043b..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardSettingsContent.styles.scss +++ /dev/null @@ -1,62 +0,0 @@ -.settings-tabs { - .ant-tabs-nav-list { - height: 32px; - flex-shrink: 0; - border-radius: 2px; - border: 1px solid var(--l1-border); - background: var(--l2-background); - 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; - } - - .ant-tabs-tab:not(:last-child) { - border-right: 1px solid var(--l1-border) !important; - } - - .overview-btn { - width: 114px; - display: flex; - align-items: center; - justify-content: center; - } - - .variables-btn { - width: 114px; - display: flex; - align-items: center; - justify-content: center; - } - - .public-dashboard-btn { - width: 150px; - display: flex; - align-items: center; - justify-content: center; - - &.disabled-btn { - opacity: 0.5; - cursor: not-allowed; - } - } - - .ant-tabs-ink-bar { - display: none; - } - - .ant-tabs-tab-active { - .overview-btn, - .variables-btn, - .public-dashboard-btn { - color: var(--primary-background); - background: var(--l1-border); - } - } - } - - .ant-tabs-nav::before { - border-bottom: none; - } -} diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/DynamicVariable/DynamicVariable.styles.scss b/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/DynamicVariable/DynamicVariable.styles.scss deleted file mode 100644 index f48aa69e20b..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/DynamicVariable/DynamicVariable.styles.scss +++ /dev/null @@ -1,31 +0,0 @@ -.dynamic-variable-container { - margin: 24px 0; - - .dynamic-variable-config-container { - display: grid; - grid-template-columns: 1fr 32px 16px 200px; - gap: 16px; - align-items: center; - width: 100%; - - .ant-select { - .ant-select-selector { - border-radius: 4px; - border: 1px solid var(--l1-border); - } - } - - .ant-input { - border-radius: 4px; - border: 1px solid var(--l1-border); - max-width: 300px; - } - - .dynamic-variable-from-text { - font-family: 'Space Mono'; - font-size: 13px; - font-weight: 500; - white-space: nowrap; - } - } -} diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/DynamicVariable/DynamicVariable.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/DynamicVariable/DynamicVariable.tsx deleted file mode 100644 index 0cdd18d54e7..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/DynamicVariable/DynamicVariable.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { - Dispatch, - SetStateAction, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; -import { Color } from '@signozhq/design-tokens'; -import { Select } from 'antd'; -import { Typography } from '@signozhq/ui/typography'; -import CustomSelect from 'components/NewSelect/CustomSelect'; -import TextToolTip from 'components/TextToolTip'; -import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; -import { useGetFieldKeys } from 'hooks/dynamicVariables/useGetFieldKeys'; -import { useIsDarkMode } from 'hooks/useDarkMode'; -import useDebounce from 'hooks/useDebounce'; -import { Info } from '@signozhq/icons'; -import { FieldKey } from 'types/api/dynamicVariables/getFieldKeys'; -import { isRetryableError as checkIfRetryableError } from 'utils/errorUtils'; - -import './DynamicVariable.styles.scss'; - -enum AttributeSource { - ALL_TELEMETRY = 'All telemetry', - LOGS = 'Logs', - METRICS = 'Metrics', - TRACES = 'Traces', -} - -function DynamicVariable({ - setDynamicVariablesSelectedValue, - dynamicVariablesSelectedValue, - errorAttributeKeyMessage, -}: { - setDynamicVariablesSelectedValue: Dispatch< - SetStateAction< - | { - name: string; - value: string; - } - | undefined - > - >; - dynamicVariablesSelectedValue: - | { - name: string; - value: string; - } - | undefined; - errorAttributeKeyMessage?: string; -}): JSX.Element { - const sources = [ - AttributeSource.ALL_TELEMETRY, - AttributeSource.LOGS, - AttributeSource.TRACES, - AttributeSource.METRICS, - ]; - - const [attributeSource, setAttributeSource] = useState(); - const [attributes, setAttributes] = useState>({}); - const [selectedAttribute, setSelectedAttribute] = useState(); - const [apiSearchText, setApiSearchText] = useState(''); - const [errorMessage, setErrorMessage] = useState(); - const [isRetryableError, setIsRetryableError] = useState(true); - const debouncedApiSearchText = useDebounce(apiSearchText, DEBOUNCE_DELAY); - - const [filteredAttributes, setFilteredAttributes] = useState< - Record - >({}); - - useEffect(() => { - if (dynamicVariablesSelectedValue?.name) { - setSelectedAttribute(dynamicVariablesSelectedValue.name); - } - - if (dynamicVariablesSelectedValue?.value) { - setAttributeSource(dynamicVariablesSelectedValue.value as AttributeSource); - } - }, [ - dynamicVariablesSelectedValue?.name, - dynamicVariablesSelectedValue?.value, - ]); - - const { data, error, isLoading, refetch } = useGetFieldKeys({ - signal: - attributeSource === AttributeSource.ALL_TELEMETRY - ? undefined - : (attributeSource?.toLowerCase() as 'traces' | 'logs' | 'metrics'), - name: debouncedApiSearchText, - }); - - const isComplete = useMemo(() => data?.data?.complete === true, [data]); - - useEffect(() => { - if (data) { - const newAttributes = data.data?.keys ?? {}; - setAttributes(newAttributes); - setFilteredAttributes(newAttributes); - } - }, [data]); - - // Handle error from useGetFieldKeys - useEffect(() => { - if (error) { - // Check if error is retryable (5xx) or not (4xx) - const isRetryable = checkIfRetryableError(error); - setIsRetryableError(isRetryable); - } - }, [error]); - - // refetch when attributeSource changes - useEffect(() => { - if (attributeSource) { - refetch(); - } - }, [attributeSource, refetch, debouncedApiSearchText]); - - // Handle search based on whether we have complete data or not - const handleSearch = useCallback( - (text: string) => { - if (isComplete) { - // If complete is true, do client-side filtering - if (!text) { - setFilteredAttributes(attributes); - return; - } - - const filtered: Record = {}; - Object.keys(attributes).forEach((key) => { - if (key.toLowerCase().includes(text.toLowerCase())) { - filtered[key] = attributes[key]; - } - }); - setFilteredAttributes(filtered); - } else { - // If complete is false, debounce the API call - setApiSearchText(text); - } - }, - [attributes, isComplete], - ); - - // update setDynamicVariablesSelectedValue with debounce when attribute and source is selected - useEffect(() => { - if (selectedAttribute || attributeSource) { - setDynamicVariablesSelectedValue({ - name: selectedAttribute || dynamicVariablesSelectedValue?.name || '', - value: - attributeSource || - dynamicVariablesSelectedValue?.value || - AttributeSource.ALL_TELEMETRY, - }); - } - }, [ - selectedAttribute, - attributeSource, - setDynamicVariablesSelectedValue, - dynamicVariablesSelectedValue?.name, - dynamicVariablesSelectedValue?.value, - ]); - - const isDarkMode = useIsDarkMode(); - const errorText = (error as any)?.message || errorMessage; - return ( -
-
- ({ - label: key, - value: key, - }))} - loading={isLoading} - status={errorText ? 'error' : undefined} - onChange={(value): void => { - setSelectedAttribute(value); - }} - showSearch - errorMessage={errorText as any} - value={selectedAttribute || dynamicVariablesSelectedValue?.name} - onSearch={handleSearch} - onRetry={(): void => { - // reset error message - setErrorMessage(undefined); - setIsRetryableError(true); - refetch(); - }} - showRetryButton={isRetryableError} - /> - from - - - } - /> - - { - setVariableName(value); - setHasUserManuallyChangedName(true); // Mark that user has manually changed the name - - // Check for empty name - if (!value.trim()) { - setErrorName(true); - setErrorNameMessage(REQUIRED_NAME_MESSAGE); - } - // Check for whitespace in name - else if (/\s/.test(value)) { - setErrorName(true); - setErrorNameMessage('Variable name cannot contain whitespaces'); - } - // Check for duplicate name - else if (!validateName(value) && value !== variableData.name) { - setErrorName(true); - setErrorNameMessage('Variable name already exists'); - } - // No errors - else { - setErrorName(false); - setErrorNameMessage(''); - } - }} - /> -
- {errorNameMessage} -
-
- - - - Description - - - setVariableDescription(e.target.value)} - /> - - - - Variable Type - - } - /> - - -
- - - - -
-
- {queryType === 'DYNAMIC' && ( -
- -
- )} - {queryType === 'QUERY' && ( -
- - Query - - -
- 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, - ); - }} - /> - ), - }, - ]} - /> - - )} - {queryType === 'TEXTBOX' && ( - - - Default Value - - { - setVariableTextboxValue(e.target.value); - }} - placeholder="Enter a default value (if any)..." - style={{ width: 400 }} - /> - - )} - {(queryType === 'QUERY' || - queryType === 'CUSTOM' || - queryType === 'DYNAMIC') && ( - <> - - - - Preview of Values - - -
- {errorPreview ? ( - {errorPreview} - ) : ( - map(previewValues, (value, idx) => ( - - {value.toString()} - - )) - )} -
-
- - - Sort Values - - - - - - - - Enable multiple values to be checked - - - { - setVariableMultiSelect(e); - if (!e) { - setVariableShowALLOption(false); - } - }} - /> - - {variableMultiSelect && queryType !== 'DYNAMIC' && ( - - - - Include an option for ALL values - - - setVariableShowALLOption(e)} - /> - - )} - - - Default Value - - {queryType === 'QUERY' - ? 'Click Test Run Query to see the values or add custom value' - : 'Select a value from the preview values or add custom value'} - - - setVariableDefaultValue(value)} - options={previewValues.map((value) => ({ - label: value, - value, - }))} - /> - - - )} - {queryType === 'DYNAMIC' && ( - - - - Select Panels to apply this variable - - - - - )} -
- -
- - - - -
- - ); -} - -export default VariableItem; diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/WidgetSelector.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/WidgetSelector.tsx deleted file mode 100644 index c02cc59827d..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/WidgetSelector.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import { CustomMultiSelect } from 'components/NewSelect'; -import { PANEL_GROUP_TYPES } from 'constants/queryBuilder'; -import { generateGridTitle } from 'container/GridPanelSwitch/utils'; -import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore'; -import { WidgetRow, Widgets } from 'types/api/dashboard/getAll'; - -export function WidgetSelector({ - selectedWidgets, - setSelectedWidgets, -}: { - selectedWidgets: string[]; - setSelectedWidgets: (widgets: string[]) => void; -}): JSX.Element { - const { dashboardData } = useDashboardStore(); - - // Get layout IDs for cross-referencing - const layoutIds = new Set( - (dashboardData?.data?.layout || []).map((item) => item.i), - ); - - // Filter and deduplicate widgets by ID, keeping only those with layout entries - // and excluding row widgets since they are not panels that can have variables - const widgets = Object.values( - (dashboardData?.data?.widgets || []).reduce( - (acc: Record, widget: WidgetRow | Widgets) => { - if ( - widget.id && - layoutIds.has(widget.id) && - widget.panelTypes !== PANEL_GROUP_TYPES.ROW - ) { - acc[widget.id] = widget; - } - return acc; - }, - {}, - ), - ); - - // Filter selectedWidgets to only include widgets that are present in the current layout - const validSelectedWidgets = selectedWidgets.filter((widgetId) => - layoutIds.has(widgetId), - ); - - // Update selectedWidgets if any invalid widgets were removed - React.useEffect(() => { - if (validSelectedWidgets.length !== selectedWidgets.length) { - setSelectedWidgets(validSelectedWidgets); - } - }, [validSelectedWidgets, selectedWidgets.length, setSelectedWidgets]); - - return ( - ({ - label: generateGridTitle(widget.title), - value: widget.id, - }))} - value={validSelectedWidgets} - onChange={(value): void => setSelectedWidgets(value as string[])} - showLabels - /> - ); -} diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/styles.ts b/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/styles.ts deleted file mode 100644 index 2d603ede186..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/styles.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Row } from 'antd'; -import styled from 'styled-components'; - -export const VariableItemRow = styled(Row)` - gap: 1rem; - margin-bottom: 1rem; -`; - -export const LabelContainer = styled.div` - width: 200px; -`; diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/addTagFiltersToDashboard.test.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/addTagFiltersToDashboard.test.tsx deleted file mode 100644 index e4f0576f0b7..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/addTagFiltersToDashboard.test.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import { PANEL_TYPES } from 'constants/queryBuilder'; -import { Dashboard } from 'types/api/dashboard/getAll'; -import { EQueryType } from 'types/common/dashboard'; -import { DataSource } from 'types/common/queryBuilder'; -import { removeVariableReferencesFromDashboard } from './addTagFiltersToDashboard'; - -// --------------------------------------------------------------------------- -// Shared fixture helpers -// --------------------------------------------------------------------------- - -const EMPTY_BUILDER = { - queryData: [] as any, - queryFormulas: [], - queryTraceOperator: [], -}; - -const BASE_WIDGET = { - opacity: '1', - nullZeroValues: 'null', - timePreferance: 'GLOBAL_TIME' as const, - softMin: null, - softMax: null, - selectedLogFields: null, - selectedTracesFields: null, -}; - -const DEFAULT_QUERY_DATA = { - queryName: 'q1', - // In QB v5, expression holds the query label (A/B/C), not a filter expression - expression: 'A', - dataSource: DataSource.METRICS, - functions: [], - groupBy: [], - filters: { items: [] as any[], op: 'AND' as const }, - legend: '', - disabled: false, - having: [], - limit: null, - stepInterval: null, - orderBy: [], - selectColumns: [], - source: '' as const, -}; - -/** - * Build a dashboard with a single builder widget. - * Only supply the fields your test actually cares about. - */ -const buildBuilderDashboard = ( - filterExpression: string, - queryDataOverrides: Record = {}, -): Dashboard => ({ - id: 'dash1', - createdAt: '', - updatedAt: '', - createdBy: '', - updatedBy: '', - data: { - title: 'Test Dashboard', - widgets: [ - { - ...BASE_WIDGET, - id: 'widget-1', - panelTypes: PANEL_TYPES.TIME_SERIES, - title: 'Widget 1', - description: '', - query: { - id: 'query1', - queryType: EQueryType.QUERY_BUILDER, - promql: [], - clickhouse_sql: [], - builder: { - queryData: [ - { - ...DEFAULT_QUERY_DATA, - ...queryDataOverrides, - filter: { expression: filterExpression }, - }, - ], - queryFormulas: [], - queryTraceOperator: [], - }, - unit: '', - }, - }, - ], - variables: {}, - }, -}); - -const buildClickhouseDashboard = (query: string): Dashboard => ({ - id: 'dash-ch', - createdAt: '', - updatedAt: '', - createdBy: '', - updatedBy: '', - data: { - title: 'CH', - widgets: [ - { - ...BASE_WIDGET, - id: 'w1', - panelTypes: PANEL_TYPES.TIME_SERIES, - title: '', - description: '', - query: { - id: 'q1', - queryType: EQueryType.CLICKHOUSE, - promql: [], - clickhouse_sql: [{ name: 'A', query, legend: '', disabled: false }], - builder: EMPTY_BUILDER, - unit: '', - }, - }, - ], - variables: {}, - }, -}); - -const buildPromqlDashboard = (query: string): Dashboard => ({ - id: 'dash-prom', - createdAt: '', - updatedAt: '', - createdBy: '', - updatedBy: '', - data: { - title: 'PromQL Dashboard', - widgets: [ - { - ...BASE_WIDGET, - id: 'widget-prom', - panelTypes: PANEL_TYPES.TIME_SERIES, - title: 'PromQL Widget', - description: '', - query: { - id: 'query-prom', - queryType: EQueryType.PROM, - promql: [{ name: 'A', query, legend: '', disabled: false }], - clickhouse_sql: [], - builder: EMPTY_BUILDER, - unit: '', - }, - }, - ], - variables: {}, - }, -}); - -/** Run removeVariableReferencesFromDashboard on a single-widget clickhouse dashboard and return the cleaned SQL. */ -const chQuery = (sql: string, varName: string): string => { - const result = removeVariableReferencesFromDashboard( - buildClickhouseDashboard(sql), - varName, - ); - return (result!.data.widgets![0] as any).query.clickhouse_sql[0].query; -}; - -/** Extract the first builder queryData from a cleaned dashboard. */ -const firstBuilderQueryData = (dashboard: Dashboard | undefined): any => - (dashboard!.data.widgets![0] as any).query.builder.queryData[0]; - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -describe('removeVariableReferencesFromDashboard', () => { - describe('builder filter expression cleanup', () => { - it('removes a variable clause from filter.expression', () => { - const dashboard = buildBuilderDashboard( - "service.name IN $service AND env = 'prod'", - ); - - const result = removeVariableReferencesFromDashboard(dashboard, 'service'); - - expect(firstBuilderQueryData(result).filter.expression).toBe("env = 'prod'"); - }); - - it('leaves no dangling AND/OR after removing a variable clause', () => { - const dashboard = buildBuilderDashboard( - "service.name IN $service AND env = 'prod'", - ); - - const result = removeVariableReferencesFromDashboard(dashboard, 'service'); - const { expression } = firstBuilderQueryData(result).filter; - - expect(expression).toBe("env = 'prod'"); - expect(expression).not.toMatch(/^\s*(AND|OR)/i); - expect(expression).not.toMatch(/(AND|OR)\s*$/i); - }); - - it('does not remove $environment clause when deleting $env', () => { - const dashboard = buildBuilderDashboard( - 'env = $env AND deployment.environment = $environment', - ); - - const result = removeVariableReferencesFromDashboard(dashboard, 'env'); - - expect(firstBuilderQueryData(result).filter.expression).toBe( - 'deployment.environment = $environment', - ); - }); - - it('leaves literal filter expressions untouched when removing a variable', () => { - const dashboard = buildBuilderDashboard( - "service.name = 'api-gateway' AND env = 'prod'", - ); - - const result = removeVariableReferencesFromDashboard(dashboard, 'service'); - - expect(firstBuilderQueryData(result).filter.expression).toBe( - "service.name = 'api-gateway' AND env = 'prod'", - ); - }); - - it('removes only the variable clause, preserving a literal clause on the same key', () => { - const dashboard = buildBuilderDashboard( - "service.name IN $service AND service.name = 'api-gateway'", - ); - - const result = removeVariableReferencesFromDashboard(dashboard, 'service'); - - expect(firstBuilderQueryData(result).filter.expression).toBe( - "service.name = 'api-gateway'", - ); - }); - - it('returns filter.expression unchanged when the variable has no clauses in it', () => { - const dashboard = buildBuilderDashboard("env = 'prod'"); - - const result = removeVariableReferencesFromDashboard(dashboard, 'service'); - - expect(firstBuilderQueryData(result).filter.expression).toBe("env = 'prod'"); - }); - }); - - describe('PromQL query cleanup', () => { - it('removes variable placeholder from a promql query', () => { - const result = removeVariableReferencesFromDashboard( - buildPromqlDashboard('sum(rate(http_requests_total{$service}[5m]))'), - 'service', - ); - - const widget = result!.data.widgets![0] as any; - expect(widget.query.promql[0].query).toBe( - 'sum(rate(http_requests_total{}[5m]))', - ); - }); - - it('strips only the variable token inside a PromQL label matcher (token-only path)', () => { - const result = removeVariableReferencesFromDashboard( - buildPromqlDashboard('up{env="$env", job="api"}'), - 'env', - ); - - const widget = result!.data.widgets![0] as any; - expect(widget.query.promql[0].query).toBe('up{env="", job="api"}'); - }); - }); - - describe('ClickHouse SQL query cleanup', () => { - it('removes a quoted variable clause and its WHERE keyword', () => { - expect( - chQuery( - "SELECT count() FROM signoz_logs WHERE service_name = '$service'", - 'service', - ), - ).toBe('SELECT count() FROM signoz_logs'); - }); - - it('removes a middle clause: AND env={{.env}} AND', () => { - expect( - chQuery('SELECT count() FROM t WHERE a=1 AND env={{.env}} AND b=2', 'env'), - ).toBe('SELECT count() FROM t WHERE a=1 AND b=2'); - }); - - it('removes the first clause: env={{.env}} AND rest', () => { - expect( - chQuery('SELECT count() FROM t WHERE env={{.env}} AND b=2', 'env'), - ).toBe('SELECT count() FROM t WHERE b=2'); - }); - - it('removes the last clause: rest AND env=$env', () => { - expect(chQuery('SELECT count() FROM t WHERE a=1 AND env=$env', 'env')).toBe( - 'SELECT count() FROM t WHERE a=1', - ); - }); - - it('removes a clause with double-bracket syntax: service=[[svc]]', () => { - expect(chQuery('SELECT count() FROM t WHERE service=[[svc]]', 'svc')).toBe( - 'SELECT count() FROM t', - ); - }); - - it('falls back to token-only strip for a bare variable in SELECT', () => { - expect(chQuery('SELECT $metric FROM table', 'metric')).toBe( - 'SELECT FROM table', - ); - }); - }); - - describe('edge cases', () => { - it('is idempotent — calling twice produces the same result', () => { - const dashboard = buildBuilderDashboard( - "service.name IN $service AND env = 'prod'", - ); - - const once = removeVariableReferencesFromDashboard(dashboard, 'service'); - const twice = removeVariableReferencesFromDashboard(once, 'service'); - - expect(twice).toStrictEqual(once); - }); - - it('handles a dashboard with no widgets without throwing', () => { - const dashboard: Dashboard = { - id: 'dash-empty', - createdAt: '', - updatedAt: '', - createdBy: '', - updatedBy: '', - data: { title: 'Empty Dashboard', widgets: undefined, variables: {} }, - }; - - expect(() => - removeVariableReferencesFromDashboard(dashboard, 'service'), - ).not.toThrow(); - }); - }); -}); diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/addTagFiltersToDashboard.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/addTagFiltersToDashboard.tsx deleted file mode 100644 index 6014480f716..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/addTagFiltersToDashboard.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import { - convertFiltersToExpressionWithExistingQuery, - createVariablePlaceholderRegExp, - removeKeysFromExpression, - removeVariableFromExpression, -} from 'components/QueryBuilderV2/utils'; -import { cloneDeep, isArray, isEmpty } from 'lodash-es'; -import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; -import { - IBuilderQuery, - TagFilterItem, -} from 'types/api/queryBuilder/queryBuilderData'; - -/** - * Updates the query filters in a builder query by appending new tag filters - */ -const updateQueryFilters = ( - queryData: IBuilderQuery, - filter: TagFilterItem, -): IBuilderQuery => { - const existingFilters = queryData.filters?.items || []; - - // addition | update - const currentFilterKey = filter.key?.key; - const valueToAdd = filter.value.toString(); - const newItems: TagFilterItem[] = []; - - existingFilters.forEach((existingFilter) => { - const newFilter = cloneDeep(existingFilter); - if ( - newFilter.key?.key === currentFilterKey && - !(isArray(newFilter.value) && newFilter.value.includes(valueToAdd)) && - newFilter.value !== valueToAdd - ) { - if (isEmpty(newFilter.value)) { - newFilter.value = valueToAdd; - newFilter.op = 'IN'; - } else { - newFilter.value = ( - isArray(newFilter.value) - ? [...newFilter.value, valueToAdd] - : [newFilter.value, valueToAdd] - ) as string[] | string; - - newFilter.op = 'IN'; - } - } - - newItems.push(newFilter); - }); - - // if yet the filter key doesn't get added then add it - if (!newItems.find((item) => item.key?.key === currentFilterKey)) { - newItems.push(filter); - } - - const newFilterToUpdate = { - ...queryData.filters, - items: newItems, - op: queryData.filters?.op || 'AND', - }; - - return { - ...queryData, - ...convertFiltersToExpressionWithExistingQuery( - newFilterToUpdate, - queryData.filter?.expression, - ), - }; -}; - -/** - * Updates a single widget by adding filters to its query - */ -const updateSingleWidget = ( - widget: Widgets, - filter: TagFilterItem, -): Widgets => { - if (!widget.query?.builder?.queryData || isEmpty(filter)) { - return widget; - } - - return { - ...widget, - query: { - ...widget.query, - builder: { - ...widget.query.builder, - queryData: widget.query.builder.queryData.map((queryData) => - updateQueryFilters(queryData, filter), - ), - }, - }, - }; -}; - -const removeIfPresent = ( - queryData: IBuilderQuery, - filter: TagFilterItem, -): IBuilderQuery => { - const existingFilters = queryData.filters?.items || []; - - // addition | update - const currentFilterKey = filter.key?.key; - const valueToAdd = filter.value.toString(); - const newItems: TagFilterItem[] = []; - - existingFilters.forEach((existingFilter) => { - const newFilter = cloneDeep(existingFilter); - if (newFilter.key?.key === currentFilterKey) { - if (isArray(newFilter.value) && newFilter.value.includes(valueToAdd)) { - newFilter.value = newFilter.value.filter((value) => value !== valueToAdd); - } else if (newFilter.value === valueToAdd) { - return; - } - } - - newItems.push(newFilter); - }); - - return { - ...queryData, - filters: { - ...queryData.filters, - items: newItems, - op: queryData.filters?.op || 'AND', - }, - filter: { - ...queryData.filter, - expression: removeKeysFromExpression( - queryData.filter?.expression ?? '', - filter.key?.key ? [filter.key.key] : [], - true, - ), - }, - }; -}; - -const updateAfterRemoval = ( - widget: Widgets, - filter: TagFilterItem, -): Widgets => { - if (!widget.query?.builder?.queryData || isEmpty(filter)) { - return widget; - } - - // remove the filters where the current filter is available as value as this widget is not selected anymore, hence removal - return { - ...widget, - query: { - ...widget.query, - builder: { - ...widget.query.builder, - queryData: widget.query.builder.queryData.map((queryData) => - removeIfPresent(queryData, filter), - ), - }, - }, - }; -}; - -const removeVariablePlaceholders = ( - text: string | undefined, - variableName: string, -): string => { - if (!text) { - return ''; - } - - const tokenPattern = createVariablePlaceholderRegExp(variableName); - - // Step 1: attempt clause-aware removal for SQL WHERE patterns. - // Strips the entire `key op $var` unit plus its adjacent AND/OR so we - // never leave a dangling `key = ` in unquoted ClickHouse SQL clauses. - // Handles three shapes: - // (a) preceding conjunction: AND key = $var - // (b) following conjunction: key = $var AND - // (c) standalone clause: key = $var (end of expression) - const escapedToken = tokenPattern.source; - const clausePattern = new RegExp( - // (a) conjunction before the clause - `\\s*\\b(?:AND|OR)\\b\\s+[\\w."'\\[\\]]+\\s*(?:=|!=|<>|LIKE|ILIKE|IN|NOT\\s+IN)\\s*'?${escapedToken}'?` + - // (b)+(c) clause first, optional conjunction after - `|[\\w."'\\[\\]]+\\s*(?:=|!=|<>|LIKE|ILIKE|IN|NOT\\s+IN)\\s*'?${escapedToken}'?(?:\\s*\\b(?:AND|OR)\\b)?`, - 'gi', - ); - - const withClauseRemoval = text.replace(clausePattern, ''); - if (withClauseRemoval !== text) { - return withClauseRemoval - .replace(/\s{2,}/g, ' ') - .replace(/\bWHERE\s*$/i, '') - .trim(); - } - - // Step 2: fallback — bare variable usage outside a key-op-value pattern - // (e.g. SELECT $metric, LIMIT $n). Token-only removal is correct here. - return text - .replace(tokenPattern, '') - .replace(/\s{2,}/g, ' ') - .trim(); -}; - -const removeVariableReferencesFromQueryData = ( - queryData: IBuilderQuery, - variableName: string, -): IBuilderQuery => { - const updatedFilter = queryData.filter?.expression - ? { - ...queryData.filter, - expression: removeVariableFromExpression( - queryData.filter.expression, - variableName, - ), - } - : queryData.filter; - - return { ...queryData, filter: updatedFilter }; -}; - -const removeVariableReferencesFromWidget = ( - widget: Widgets, - variableName: string, -): Widgets => { - let updatedWidget = { ...widget }; - - if (updatedWidget.query?.builder?.queryData) { - updatedWidget = { - ...updatedWidget, - query: { - ...updatedWidget.query, - builder: { - ...updatedWidget.query.builder, - queryData: updatedWidget.query.builder.queryData.map((queryData) => - removeVariableReferencesFromQueryData(queryData, variableName), - ), - }, - }, - }; - } - - if (updatedWidget.query?.promql) { - updatedWidget = { - ...updatedWidget, - query: { - ...updatedWidget.query, - promql: updatedWidget.query.promql.map((promqlQuery) => ({ - ...promqlQuery, - query: removeVariablePlaceholders(promqlQuery.query, variableName), - })), - }, - }; - } - - if (updatedWidget.query?.clickhouse_sql) { - updatedWidget = { - ...updatedWidget, - query: { - ...updatedWidget.query, - clickhouse_sql: updatedWidget.query.clickhouse_sql.map((sqlQuery) => ({ - ...sqlQuery, - query: removeVariablePlaceholders(sqlQuery.query, variableName), - })), - }, - }; - } - - return updatedWidget; -}; - -export const removeVariableReferencesFromDashboard = ( - dashboard: Dashboard | undefined, - variableName: string, -): Dashboard | undefined => { - if (!dashboard || !variableName) { - return dashboard; - } - - const updatedDashboard = cloneDeep(dashboard); - - if (updatedDashboard.data.widgets) { - updatedDashboard.data.widgets = updatedDashboard.data.widgets.map( - (widget) => { - if ('query' in widget) { - return removeVariableReferencesFromWidget(widget as Widgets, variableName); - } - return widget; - }, - ); - } - - return updatedDashboard; -}; - -/** - * A function that takes a dashboard configuration and a list of tag filters - * and returns an updated dashboard with the filters appended to widget queries. - * - * @param dashboard The dashboard configuration - * @param filters Array of tag filters to apply to widgets - * @param widgetIds Optional array of widget IDs to filter which widgets get updated - * @returns Updated dashboard configuration with filters applied - */ -export const addTagFiltersToDashboard = ( - dashboard: Dashboard | undefined, - filter: TagFilterItem, - widgetIds?: string[], - applyToAll?: boolean, -): Dashboard | undefined => { - if (!dashboard || isEmpty(filter)) { - return dashboard; - } - - // Create a deep copy to avoid mutating the original dashboard - const updatedDashboard = cloneDeep(dashboard); - - // Process each widget to add filters - if (updatedDashboard.data.widgets) { - updatedDashboard.data.widgets = updatedDashboard.data.widgets.map( - (widget) => { - // Only apply to widgets with 'query' property - if ('query' in widget) { - // If widgetIds is provided, only update widgets with matching IDs - if (!applyToAll && widgetIds && !widgetIds.includes(widget.id)) { - // removal if needed - return updateAfterRemoval(widget as Widgets, filter); - } - return updateSingleWidget(widget as Widgets, filter); - } - return widget; - }, - ); - } - - return updatedDashboard; -}; diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/index.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/index.tsx deleted file mode 100644 index f39e9955b49..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/index.tsx +++ /dev/null @@ -1,537 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { GripVertical, PenLine, Plus, Trash2 } from '@signozhq/icons'; -import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core'; -import { - DndContext, - PointerSensor, - useSensor, - useSensors, -} from '@dnd-kit/core'; -import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; -import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; -import { Button, Modal, Row, RowProps, Space, Table, Flex } from 'antd'; -import { Typography } from '@signozhq/ui/typography'; -import { VariablesSettingsTabHandle } from 'container/DashboardContainer/DashboardDescription/types'; -import { convertVariablesToDbFormat } from 'container/DashboardContainer/DashboardVariablesSelection/util'; -import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels'; -import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables'; -import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; -import { toast } from '@signozhq/ui/sonner'; -import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes'; -import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; -import { removeVariableReferencesFromDashboard } from './addTagFiltersToDashboard'; - -import { TVariableMode } from './types'; -import VariableItem from './VariableItem/VariableItem'; - -import '../DashboardSettings.styles.scss'; - -function TableRow({ children, ...props }: RowProps): JSX.Element { - const { - attributes, - listeners, - setNodeRef, - setActivatorNodeRef, - transform, - transition, - isDragging, - } = useSortable({ - // @ts-expect-error - id: props['data-row-key'], - }); - - const style: React.CSSProperties = { - ...props.style, - transform: CSS.Transform.toString(transform && { ...transform, scaleY: 1 }), - transition, - ...(isDragging ? { position: 'relative', zIndex: 9999 } : {}), - }; - - return ( - - {React.Children.map(children, (child) => { - const childElement = child as React.ReactElement; - if (childElement.key === 'name') { - return React.cloneElement(childElement, { - key: 'name-with-drag', - children: ( -
- } - style={{ touchAction: 'none', cursor: 'move' }} - size="md" - {...listeners} - /> - - {child} -
- ), - }); - } - - return childElement; - })} - - ); -} - -function VariablesSettings({ - variablesSettingsTabHandle, -}: { - variablesSettingsTabHandle: VariablesSettingsTabHandle; -}): JSX.Element { - const variableToDelete = useRef(null); - const [deleteVariableModal, setDeleteVariableModal] = useState(false); - const variableToApplyToAll = useRef(null); - const [applyToAllModal, setApplyToAllModal] = useState(false); - - const { t } = useTranslation(['dashboard']); - - const { dashboardData, setDashboardData } = useDashboardStore(); - const { dashboardVariables } = useDashboardVariables(); - - const [variablesTableData, setVariablesTableData] = useState([]); - const [variblesOrderArr, setVariablesOrderArr] = useState([]); - const [existingVariableNamesMap, setExistingVariableNamesMap] = useState< - Record - >({}); - - const [variableViewMode, setVariableViewMode] = useState( - null, - ); - - const [variableEditData, setVariableEditData] = - useState(null); - - const onDoneVariableViewMode = (): void => { - setVariableViewMode(null); - setVariableEditData(null); - }; - - const onVariableViewModeEnter = ( - viewType: TVariableMode, - varData: IDashboardVariable, - ): void => { - setVariableEditData(varData); - setVariableViewMode(viewType); - }; - - useEffect(() => { - if (!variablesSettingsTabHandle) { - return; - } - - variablesSettingsTabHandle.current = { - resetState: onDoneVariableViewMode, - }; - }, [variablesSettingsTabHandle]); - - const updateMutation = useUpdateDashboard(); - - const addDynamicVariableToPanels = useAddDynamicVariableToPanels(); - - useEffect(() => { - const tableRowData = []; - const variableOrderArr = []; - const variableNamesMap = {}; - - for (const [key, value] of Object.entries(dashboardVariables)) { - const { order, id, name } = value; - - tableRowData.push({ - key, - name: key, - ...dashboardVariables[key], - id, - }); - - if (name) { - // @ts-expect-error - variableNamesMap[name] = name; - } - - if (order) { - variableOrderArr.push(order); - } - } - - tableRowData.sort((a, b) => a.order - b.order); - variableOrderArr.sort((a, b) => a - b); - - setVariablesTableData(tableRowData); - setVariablesOrderArr(variableOrderArr); - setExistingVariableNamesMap(variableNamesMap); - }, [dashboardVariables]); - - const updateVariables = ( - updatedVariablesData: IDashboardVariables, - currentRequestedId?: string, - widgetIds?: string[], - applyToAll?: boolean, - ): void => { - if (!dashboardData) { - return; - } - - const newDashboard = - (currentRequestedId && - updatedVariablesData[currentRequestedId || '']?.type === 'DYNAMIC' && - addDynamicVariableToPanels( - dashboardData, - updatedVariablesData[currentRequestedId || ''], - widgetIds, - applyToAll, - )) || - dashboardData; - - updateMutation.mutateAsync( - { - id: dashboardData.id, - - data: { - ...newDashboard.data, - variables: updatedVariablesData, - }, - }, - { - onSuccess: (updatedDashboard) => { - if (updatedDashboard.data) { - setDashboardData(updatedDashboard.data); - toast.success(t('variable_updated_successfully')); - } - }, - }, - ); - }; - - const getVariableOrder = (): number => { - if (variblesOrderArr && variblesOrderArr.length > 0) { - return variblesOrderArr[variblesOrderArr.length - 1] + 1; - } - - return 0; - }; - - const onVariableSaveHandler = ( - mode: TVariableMode, - variableData: IDashboardVariable, - widgetIds?: string[], - applyToAll?: boolean, - ): void => { - const updatedVariableData = { - ...variableData, - order: variableData?.order >= 0 ? variableData.order : getVariableOrder(), - }; - - const newVariablesArr = variablesTableData.map( - (variable: IDashboardVariable) => { - if (variable.id === updatedVariableData.id) { - return updatedVariableData; - } - - return variable; - }, - ); - - if (mode === 'ADD') { - newVariablesArr.push(updatedVariableData); - } - - const variables = convertVariablesToDbFormat(newVariablesArr); - - setVariablesTableData(newVariablesArr); - updateVariables(variables, variableData?.id, widgetIds, applyToAll); - onDoneVariableViewMode(); - }; - - const onVariableDeleteHandler = (variable: IDashboardVariable): void => { - variableToDelete.current = variable; - setDeleteVariableModal(true); - }; - - const handleDeleteConfirm = (): void => { - if (!dashboardData || !variableToDelete.current) { - setDeleteVariableModal(false); - return; - } - - const newVariablesArr = variablesTableData.filter( - (variable: IDashboardVariable) => - variable.id !== variableToDelete?.current?.id, - ); - - const updatedVariables = convertVariablesToDbFormat(newVariablesArr); - - const cleanedDashboard = - removeVariableReferencesFromDashboard( - dashboardData, - variableToDelete.current.name || '', - ) || dashboardData; - - updateMutation.mutateAsync( - { - id: dashboardData.id, - - data: { - ...cleanedDashboard.data, - variables: updatedVariables, - }, - }, - { - onSuccess: (updatedDashboard) => { - if (updatedDashboard.data) { - setDashboardData(updatedDashboard.data); - toast.success(t('variable_updated_successfully')); - } - }, - }, - ); - - variableToDelete.current = null; - setDeleteVariableModal(false); - }; - const handleDeleteCancel = (): void => { - variableToDelete.current = null; - setDeleteVariableModal(false); - }; - - const onApplyToAllHandler = (variable: IDashboardVariable): void => { - variableToApplyToAll.current = variable; - setApplyToAllModal(true); - }; - - const handleApplyToAllConfirm = (): void => { - if (variableToApplyToAll.current) { - onVariableSaveHandler( - variableViewMode || 'EDIT', - variableToApplyToAll.current, - [], - true, - ); - } - variableToApplyToAll.current = null; - setApplyToAllModal(false); - }; - - const handleApplyToAllCancel = (): void => { - variableToApplyToAll.current = null; - setApplyToAllModal(false); - }; - - const validateVariableName = (name: string): boolean => - !existingVariableNamesMap[name]; - - const validateAttributeKey = ( - attributeKey: string, - currentVariableId?: string, - ): boolean => { - // Check if any other dynamic variable already uses this attribute key - const isDuplicateAttributeKey = Object.values(dashboardVariables).some( - (variable: IDashboardVariable) => - variable.type === 'DYNAMIC' && - variable.dynamicVariablesAttribute === attributeKey && - variable.id !== currentVariableId, // Exclude current variable being edited - ); - return !isDuplicateAttributeKey; - }; - - const columns = [ - { - title: 'Variable', - dataIndex: 'name', - width: '50%', - key: 'name', - }, - { - title: 'Description', - width: '50%', - key: 'description', - render: (variable: IDashboardVariable): JSX.Element => ( -
- - {variable.description} - - - {variable.type === 'DYNAMIC' && ( - - )} - - - -
- ), - }, - ]; - - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - // https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints - distance: 1, - }, - }), - ); - - const onDragEnd = ({ active, over }: DragEndEvent): void => { - if (active.id !== over?.id) { - const activeIndex = variablesTableData.findIndex( - (i: { key: UniqueIdentifier }) => i.key === active.id, - ); - const overIndex = variablesTableData.findIndex( - (i: { key: UniqueIdentifier | undefined }) => i.key === over?.id, - ); - - const updatedVariables: IDashboardVariable[] = arrayMove( - variablesTableData, - activeIndex, - overIndex, - ); - - const reArrangedVariables = {}; - - for (let index = 0; index < updatedVariables.length; index += 1) { - const variableName = updatedVariables[index].name; - - if (variableName) { - // @ts-expect-error - reArrangedVariables[variableName] = { - ...updatedVariables[index], - order: index, - }; - } - } - - updateVariables(reArrangedVariables); - - setVariablesTableData(updatedVariables); - } - }; - - return ( - <> - {variableViewMode ? ( - - ) : ( - <> - - - - - - variable.key)} - > - - - - - )} - - - Are you sure you want to delete variable{' '} - - {variableToDelete?.current?.name} - - ? - - - - - Are you sure you want to apply variable{' '} - - {variableToApplyToAll?.current?.name} - {' '} - to all panels? This action may affect panels where this variable is not - applicable. - - - - ); -} - -export default VariablesSettings; diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/types.ts b/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/types.ts deleted file mode 100644 index 43334660dff..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/DashboardVariableSettings/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type TVariableMode = 'VIEW' | 'EDIT' | 'ADD'; - -export const VariableModes = { - VIEW: 'VIEW', - EDIT: 'EDIT', - ADD: 'ADD', -}; diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/General/AddBadges/AddBadges.module.scss b/frontend/src/container/DashboardContainer/DashboardSettings/General/AddBadges/AddBadges.module.scss deleted file mode 100644 index 3595128141f..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/General/AddBadges/AddBadges.module.scss +++ /dev/null @@ -1,49 +0,0 @@ -.badgesContainer { - display: flex; - align-items: center; - flex-flow: wrap; - gap: 6px; -} - -.badgeContainer { - color: var(--bg-sienna-400); - font-family: 'Space Mono'; - font-size: 13px; - font-style: normal; - font-weight: 500; - line-height: 20px; - letter-spacing: 0.52px; - height: 24px; - flex-shrink: 0; - border-radius: 50px; - border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent); - background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent); - padding: 2px 8px; - display: flex; - justify-content: space-between; - align-items: center; -} - -.inputContainer { - > div { - margin: 0; - } - padding-left: 0px !important; - padding-right: 0px !important; -} - -.tagsInput { - display: flex; - border: none; - padding: 0px; - width: 183px; - height: 24px; - padding: 3px 1px; - flex-shrink: 0; -} - -.editInput { - .ant-form-item { - margin-bottom: 0px; - } -} diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/General/AddBadges/index.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/General/AddBadges/index.tsx deleted file mode 100644 index ffc894787c7..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/General/AddBadges/index.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { Dispatch, SetStateAction, useState } from 'react'; -import { Col, Tooltip } from 'antd'; -import { Badge } from '@signozhq/ui/badge'; -import Input from 'components/Input'; - -import styles from './AddBadges.module.scss'; - -function AddTags({ tags, setTags }: AddTagsProps): JSX.Element { - const [inputValue, setInputValue] = useState(''); - const [editInputIndex, setEditInputIndex] = useState(-1); - const [editInputValue, setEditInputValue] = useState(''); - - const handleInputConfirm = (): void => { - if (inputValue) { - setTags([...tags, inputValue]); - } - setInputValue(''); - }; - - const handleEditInputConfirm = (): void => { - const newTags = [...tags]; - newTags[editInputIndex] = editInputValue; - setTags(newTags); - setEditInputIndex(-1); - setInputValue(''); - }; - - const handleClose = (removedTag: string): void => { - const newTags = tags.filter((tag) => tag !== removedTag); - setTags(newTags); - }; - - const onChangeHandler = ( - value: string, - func: Dispatch>, - ): void => { - func(value); - }; - - return ( -
- {tags.map((tag, index) => { - if (editInputIndex === index) { - return ( -
- - onChangeHandler(event.target.value, setEditInputValue) - } - onBlurHandler={handleEditInputConfirm} - onPressEnterHandler={handleEditInputConfirm} - /> - - ); - } - - const isLongTag = tag.length > 20; - - const tagElem = ( - { - e.preventDefault(); - handleClose(tag); - }} - > - { - setEditInputIndex(index); - setEditInputValue(tag); - e.preventDefault(); - }} - > - {isLongTag ? `${tag.slice(0, 20)}...` : tag} - - - ); - - return isLongTag ? ( - - {tagElem} - - ) : ( - tagElem - ); - })} - - - - onChangeHandler(event.target.value, setInputValue) - } - onBlurHandler={handleInputConfirm} - onPressEnterHandler={handleInputConfirm} - /> - - - ); -} - -interface AddTagsProps { - tags: string[]; - setTags: Dispatch>; -} - -export default AddTags; diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/General/GeneralSettings.module.scss b/frontend/src/container/DashboardContainer/DashboardSettings/General/GeneralSettings.module.scss deleted file mode 100644 index c454202dc53..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/General/GeneralSettings.module.scss +++ /dev/null @@ -1,227 +0,0 @@ -.overviewContent { - display: flex; - flex-direction: column; - gap: 16px; -} - -.overviewSettings { - border-radius: 3px; - border: 1px solid var(--l1-border); - padding: 16px !important; -} - -.crossPanelSyncGroup { - display: flex; - flex-direction: column; - gap: 16px; -} - -.crossPanelSyncSectionTitle { - color: var(--l1-foreground); - font-family: Inter; - font-size: 14px; - font-weight: 500; - line-height: 20px; -} - -.crossPanelSyncSectionHeader { - display: flex; - align-items: center; - gap: 6px; - align-self: flex-start; -} - -.crossPanelSyncInfoIcon { - cursor: help; - color: var(--l3-foreground); -} - -.crossPanelSyncTooltipContent { - display: flex; - flex-direction: column; - gap: 8px; - max-width: 300px; -} - -.crossPanelSyncTooltipTitle { - font-size: 14px; -} - -.crossPanelSyncTooltipDescription { - font-size: 12px; - line-height: 1.5; -} - -.crossPanelSyncTooltipDocLink { - display: flex; - align-items: center; - gap: 4px; - color: var(--primary-background); - font-size: 12px; - margin-top: 4px; -} - -.crossPanelSyncRow { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - gap: 16px; - - & + & { - padding-top: 16px; - border-top: 1px solid var(--l1-border); - } -} - -.crossPanelSyncInfo { - display: flex; - flex-direction: column; - gap: 4px; -} - -.crossPanelSyncTitle { - color: var(--l2-foreground); - font-family: Inter; - font-size: 14px; - font-weight: 400; - line-height: 20px; -} - -.crossPanelSyncDescription { - color: var(--l3-foreground); - font-family: Inter; - font-size: 13px; - font-weight: 400; - line-height: 20px; -} - -.nameIconInput { - display: flex; -} - -.dashboardImageInput { - :global(.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(--l1-border) !important; - background: var(--l3-background) !important; - - :global(.ant-select-selection-item) { - display: flex; - align-items: center; - } - } - - &:global(.ant-select-dropdown) { - padding: 0px !important; - } - - :global(.ant-select-item) { - padding: 0px; - align-items: center; - justify-content: center; - - :global(.ant-select-item-option-content) { - display: flex; - align-items: center; - justify-content: center; - } - } -} - -.listItemImage { - height: 16px; - width: 16px; -} - -.dashboardNameInput { - border-radius: 0px 2px 2px 0px; - border: 1px solid var(--l1-border); - background: var(--l3-background); -} - -.dashboardName { - color: var(--l2-foreground); - font-family: Inter; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 20px; - margin-bottom: 0.5rem; -} - -.descriptionTextArea { - padding: 6px 6px 6px 8px; - border-radius: 2px; - border: 1px solid var(--l1-border); - background: var(--l3-background); -} - -.overviewSettingsFooter { - 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(--l1-border); - background: var(--l2-background); -} - -.unsaved { - display: flex; - align-items: center; - gap: 8px; -} - -.unsavedDot { - width: 6px; - height: 6px; - border-radius: 50px; - background: var(--primary-background); - box-shadow: 0px 0px 6px 0px - color-mix(in srgb, var(--primary-background) 40%, transparent); -} - -.unsavedChanges { - color: var(--bg-robin-400); - font-family: Inter; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 24px; - letter-spacing: -0.07px; -} - -.footerActionBtns { - display: flex; - gap: 8px; -} - -.discardBtn { - margin: '16px 0'; - color: var(--l1-foreground); - font-family: Inter; - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 24px; -} - -.saveBtn { - margin: 0px !important; - color: var(--l1-foreground); - font-family: Inter; - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 24px; -} diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/General/index.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/General/index.tsx deleted file mode 100644 index 2adc612707a..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/General/index.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Col, Input, Select, Space, Tooltip } from 'antd'; -import { ToggleGroupSimple } from '@signozhq/ui/toggle-group'; -import { Typography } from '@signozhq/ui/typography'; -import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddBadges'; -import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode'; -import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode'; -import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; -import { - DashboardCursorSync, - SyncTooltipFilterMode, -} from 'lib/uPlotV2/plugins/TooltipPlugin/types'; -import { isEqual } from 'lodash-es'; -import { Check, ExternalLink, SolidInfoCircle, X } from '@signozhq/icons'; -import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore'; - -import styles from './GeneralSettings.module.scss'; -import { Button } from './styles'; -import { Base64Icons } from './utils'; -import logEvent from 'api/common/logEvent'; -import { Events } from 'constants/events'; -import { getAbsoluteUrl } from 'utils/basePath'; - -const { Option } = Select; - -function GeneralDashboardSettings(): JSX.Element { - const { dashboardData, setDashboardData } = useDashboardStore(); - - const updateDashboardMutation = useUpdateDashboard(); - - const [cursorSyncMode, setCursorSyncMode] = useDashboardCursorSyncMode( - dashboardData?.id, - ); - - const [syncTooltipFilterMode, setSyncTooltipFilterMode] = - useSyncTooltipFilterMode(dashboardData?.id); - - const selectedData = dashboardData?.data; - - 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'); - - const onSaveHandler = (): void => { - if (!dashboardData) { - return; - } - - updateDashboardMutation.mutate( - { - id: dashboardData.id, - data: { - ...dashboardData.data, - description: updatedDescription, - tags: updatedTags, - title: updatedTitle, - image: updatedImage, - }, - }, - { - onSuccess: (updatedDashboard) => { - if (updatedDashboard.data) { - setDashboardData(updatedDashboard.data); - } - }, - onError: () => {}, - }, - ); - }; - - useEffect(() => { - let numberOfUnsavedChanges = 0; - const initialValues = [title, description, tags, image]; - const updatedValues = [ - updatedTitle, - updatedDescription, - updatedTags, - updatedImage, - ]; - initialValues.forEach((val, index) => { - if (!isEqual(val, updatedValues[index])) { - numberOfUnsavedChanges += 1; - } - }); - setNumberOfUnsavedChanges(numberOfUnsavedChanges); - }, [ - description, - image, - tags, - title, - updatedDescription, - updatedImage, - updatedTags, - updatedTitle, - ]); - - const discardHandler = (): void => { - setUpdatedTitle(title); - setUpdatedImage(image); - setUpdatedTags(tags); - setUpdatedDescription(description); - }; - - return ( -
-
- -
- Dashboard Name -
- - setUpdatedTitle(e.target.value)} - /> -
-
- -
- Description - setUpdatedDescription(e.target.value)} - /> -
-
- Tags - -
-
- - -
- - Cross-Panel Sync - - - - Cross-Panel Sync - - - Sync crosshair and tooltip across all the dashboard panels - - - Learn more - - -
- } - placement="top" - mouseEnterDelay={0.5} - > - - - -
-
- - Sync Mode - - - Sync crosshair and tooltip across all the dashboard panels - -
- { - setCursorSyncMode(value as DashboardCursorSync); - }} - items={[ - { value: DashboardCursorSync.None, label: 'No Sync' }, - { value: DashboardCursorSync.Crosshair, label: 'Crosshair' }, - { value: DashboardCursorSync.Tooltip, label: 'Tooltip' }, - ]} - /> -
- {cursorSyncMode === DashboardCursorSync.Tooltip && ( -
-
- - Synced Tooltip Series - - - Show only series that intersect on group-by, or every series with the - matching ones highlighted - -
- { - logEvent(Events.TOOLTIP_SYNC_MODE_CHANGED, { - path: getAbsoluteUrl(window.location.pathname), - mode: value, - }); - setSyncTooltipFilterMode(value as SyncTooltipFilterMode); - }} - items={[ - { value: SyncTooltipFilterMode.All, label: 'All' }, - { value: SyncTooltipFilterMode.Filtered, label: 'Filtered' }, - ]} - /> -
- )} - - {numberOfUnsavedChanges > 0 && ( -
-
-
- - {numberOfUnsavedChanges} unsaved change - {numberOfUnsavedChanges > 1 && 's'} - -
-
- - -
-
- )} -
- ); -} - -export default GeneralDashboardSettings; diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/General/styles.ts b/frontend/src/container/DashboardContainer/DashboardSettings/General/styles.ts deleted file mode 100644 index 43bcccef51a..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/General/styles.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Button as ButtonComponent, Drawer } from 'antd'; -import styled from 'styled-components'; - -export const Container = styled.div` - margin-top: 0.5rem; -`; - -export const Button = styled(ButtonComponent)` - &&& { - display: flex; - align-items: center; - } -`; - -export const DrawerContainer = styled(Drawer)` - .ant-drawer-header { - padding: 0; - border: none; - } -`; diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/General/utils.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/General/utils.tsx deleted file mode 100644 index 9f895c7accd..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/General/utils.tsx +++ /dev/null @@ -1,22 +0,0 @@ -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/DashboardContainer/DashboardSettings/PublicDashboard/PublicDashboard.styles.scss b/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/PublicDashboard.styles.scss deleted file mode 100644 index eb8862a2de0..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/PublicDashboard.styles.scss +++ /dev/null @@ -1,109 +0,0 @@ -.public-dashboard-setting-container { - border-radius: 3px; - border: 1px solid var(--l1-border); - padding: 16px !important; - - .public-dashboard-setting-content { - display: flex; - flex-direction: column; - gap: 8px; - - .public-dashboard-setting-content-title { - margin-bottom: 16px; - } - - .timerange-enabled-checkbox { - margin-bottom: 8px; - } - - .default-time-range-select { - margin-bottom: 8px; - - .default-time-range-select-label { - margin-bottom: 4px; - - .default-time-range-select-label-text { - font-size: 12px; - font-weight: 500; - } - } - - .ant-select-selector { - display: flex; - height: 32px; - padding: 6px; - justify-content: center; - align-items: center; - border-radius: 2px 0px 0px 2px; - border: 1px solid var(--l1-border); - background: var(--l3-background); - - .ant-select-selection-item { - display: flex; - align-items: center; - - .list-item-image { - height: 16px; - width: 16px; - } - } - } - } - - .public-dashboard-url { - .url-label-container { - margin-bottom: 4px; - - .url-label { - font-size: 12px; - font-weight: 500; - } - } - - .url-container { - display: flex; - align-items: center; - gap: 8px; - border: 1px solid var(--l1-border); - background: var(--l3-background); - border-radius: 4px; - padding: 0px 4px; - - .url-text { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - } - - .default-time-range-select-dropdown { - width: 200px; - } - - .public-dashboard-setting-callout { - margin-top: 12px; - background: color-mix(in srgb, var(--primary-background) 10%, transparent); - padding: 12px 8px; - border-radius: 3px; - - .public-dashboard-setting-callout-text { - display: flex; - align-items: center; - gap: 8px; - - font-size: 11px; - font-weight: 400; - color: var(--text-robin-300); - } - } - } - - .public-dashboard-setting-actions { - margin-top: 32px; - display: flex; - gap: 8px; - justify-content: flex-end; - } -} diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/__tests__/PublicDashboard.test.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/__tests__/PublicDashboard.test.tsx deleted file mode 100644 index 4f3df7280fc..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/__tests__/PublicDashboard.test.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import { useCopyToClipboard } from 'react-use'; -import { toast } from '@signozhq/ui/sonner'; -import { fireEvent, within } from '@testing-library/react'; -import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; -import { StatusCodes } from 'http-status-codes'; -import { - publishedPublicDashboardMeta, - unpublishedPublicDashboardMeta, -} from 'mocks-server/__mockdata__/publicDashboard'; -import { rest, server } from 'mocks-server/server'; -import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore'; -import { render, screen, userEvent, waitFor } from 'tests/test-utils'; -import { USER_ROLES } from 'types/roles'; - -import PublicDashboardSetting from '../index'; - -// Mock dependencies -jest.mock('providers/Dashboard/store/useDashboardStore'); -jest.mock('react-use', () => ({ - ...jest.requireActual('react-use'), - useCopyToClipboard: jest.fn(), -})); -jest.mock('@signozhq/ui/sonner', () => ({ - ...jest.requireActual('@signozhq/ui/sonner'), - toast: { - success: jest.fn(), - error: jest.fn(), - }, -})); - -const mockUseDashboard = jest.mocked(useDashboardStore); -const mockUseCopyToClipboard = jest.mocked(useCopyToClipboard); -const mockToast = jest.mocked(toast); - -// Test constants -const MOCK_DASHBOARD_ID = 'test-dashboard-id'; -const MOCK_PUBLIC_PATH = '/public/dashboard/test-dashboard-id'; -const DASHBOARD_VARIABLES_WARNING = - "Dashboard variables won't work in public dashboards"; - -// Use wildcard pattern to match both relative and absolute URLs in MSW -const publicDashboardURL = `*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`; - -const mockDashboardData = { - id: MOCK_DASHBOARD_ID, - data: { - title: 'Test Dashboard', - widgets: [], - layout: [], - panelMap: {}, - variables: {}, - }, -}; - -beforeAll(() => { - server.listen(); -}); - -afterAll(() => { - server.close(); -}); - -const mockSetCopyPublicDashboardURL = jest.fn(); - -beforeEach(() => { - jest.clearAllMocks(); - - // Mock window.open - window.open = jest.fn(); - - // Mock useDashboardStore - mockUseDashboard.mockReturnValue({ - dashboardData: mockDashboardData, - } as unknown as ReturnType); - - // Mock useCopyToClipboard - mockUseCopyToClipboard.mockReturnValue([ - undefined, - mockSetCopyPublicDashboardURL, - ] as unknown as ReturnType); -}); - -afterEach(() => { - server.resetHandlers(); - jest.clearAllMocks(); -}); - -describe('PublicDashboardSetting', () => { - describe('Unpublished Dashboard', () => { - it('Unpublished dashboard should be handled correctly', async () => { - server.use( - rest.get( - `*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`, - (_req, res, ctx) => - res( - ctx.status(StatusCodes.NOT_FOUND), - ctx.json(unpublishedPublicDashboardMeta), - ), - ), - ); - - render(); - - await waitFor(() => { - expect( - screen.getByText( - /This dashboard is private. Publish it to make it accessible to anyone with the link./i, - ), - ).toBeInTheDocument(); - }); - - await expect( - screen.findByRole('checkbox', { name: /enable time range/i }), - ).resolves.toBeInTheDocument(); - - await expect( - screen.findByText(/default time range/i), - ).resolves.toBeInTheDocument(); - - expect(screen.getByText(/Last 30 minutes/i)).toBeInTheDocument(); - - await waitFor(() => { - expect( - screen.getByText(new RegExp(DASHBOARD_VARIABLES_WARNING, 'i')), - ).toBeInTheDocument(); - }); - - await expect( - screen.findByRole('button', { name: /publish dashboard/i }), - ).resolves.toBeInTheDocument(); - }); - }); - - describe('Published Dashboard', () => { - it('Published dashboard should be handled correctly', async () => { - server.use( - rest.get( - `*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`, - (_req, res, ctx) => - res(ctx.status(StatusCodes.OK), ctx.json(publishedPublicDashboardMeta)), - ), - ); - - render(); - - await waitFor(() => { - expect( - screen.getByText( - /This dashboard is publicly accessible. Anyone with the link can view it./i, - ), - ).toBeInTheDocument(); - }); - - await expect( - screen.findByRole('checkbox', { name: /enable time range/i }), - ).resolves.toBeChecked(); - - await waitFor(() => { - expect(screen.getByText(/default time range/i)).toBeInTheDocument(); - }); - - expect(screen.getByText(/Last 30 minutes/i)).toBeInTheDocument(); - - await waitFor(() => { - expect(screen.getByText(/Public Dashboard URL/i)).toBeInTheDocument(); - }); - - await expect( - screen.findByRole('button', { name: /update published dashboard/i }), - ).resolves.toBeInTheDocument(); - - await expect( - screen.findByRole('button', { name: /unpublish dashboard/i }), - ).resolves.toBeInTheDocument(); - }); - }); - - describe('Time Range Settings', () => { - beforeEach(() => { - server.use( - rest.get( - `*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`, - (_req, res, ctx) => - res(ctx.status(StatusCodes.OK), ctx.json(publishedPublicDashboardMeta)), - ), - ); - }); - - it('should toggle time range enabled when checkbox is clicked', async () => { - const user = userEvent.setup({ pointerEventsCheck: 0 }); - - render(); - - // Wait for checkbox to be rendered and verify initial state - const checkbox = await screen.findByRole('checkbox', { - name: /enable time range/i, - }); - expect(checkbox).toBeChecked(); - - await user.click(checkbox); - - await waitFor(() => { - expect(checkbox).not.toBeChecked(); - }); - }); - - it('should update default time range when select value changes', async () => { - render(); - - const selectContainer = await screen.findByTestId( - 'default-time-range-select-dropdown', - ); - - const combobox = within(selectContainer).getByRole('combobox'); - - fireEvent.mouseDown(combobox); - - await screen.findByRole('listbox'); - - const option = await screen.findByText(/Last 1 hour/i, { - selector: '.ant-select-item-option-content', - }); - fireEvent.click(option); - - await waitFor(() => { - expect( - within(selectContainer).getByText(/Last 1 hour/i), - ).toBeInTheDocument(); - }); - }); - }); - - describe('Create Public Dashboard', () => { - it('should call create API when publish button is clicked', async () => { - const user = userEvent.setup({ pointerEventsCheck: 0 }); - let createApiCalled = false; - - server.use( - rest.get(publicDashboardURL, (_req, res, ctx) => - res( - ctx.status(StatusCodes.OK), - ctx.json({ - data: { - timeRangeEnabled: true, - defaultTimeRange: DEFAULT_TIME_RANGE, - publicPath: '', - }, - }), - ), - ), - rest.post(publicDashboardURL, async (req, res, ctx) => { - const body = await req.json(); - createApiCalled = true; - expect(body).toStrictEqual({ - timeRangeEnabled: true, - defaultTimeRange: DEFAULT_TIME_RANGE, - }); - return res( - ctx.status(StatusCodes.CREATED), - ctx.json({ - data: { - timeRangeEnabled: true, - defaultTimeRange: DEFAULT_TIME_RANGE, - publicPath: MOCK_PUBLIC_PATH, - }, - }), - ); - }), - ); - - render(); - - // Find and click publish button - const publishButton = await screen.findByRole('button', { - name: /publish dashboard/i, - }); - await user.click(publishButton); - - await waitFor(() => { - expect(createApiCalled).toBe(true); - expect(mockToast.success).toHaveBeenCalledWith( - 'Public dashboard created successfully', - ); - }); - }); - }); - - describe('Update Public Dashboard', () => { - it('should call update API when update button is clicked', async () => { - const user = userEvent.setup({ pointerEventsCheck: 0 }); - let updateApiCalled = false; - let capturedRequestBody: { - timeRangeEnabled: boolean; - defaultTimeRange: string; - } | null = null; - - server.use( - rest.get( - `*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`, - (_req, res, ctx) => - res(ctx.status(StatusCodes.OK), ctx.json(publishedPublicDashboardMeta)), - ), - rest.put(publicDashboardURL, async (req, res, ctx) => { - const body = await req.json(); - updateApiCalled = true; - capturedRequestBody = body; - return res(ctx.status(StatusCodes.NO_CONTENT), ctx.json({})); - }), - ); - - render(); - - // Wait for API response and component update - const updateButton = await screen.findByRole( - 'button', - { name: /update published dashboard/i }, - { timeout: 5000 }, - ); - await user.click(updateButton); - - await waitFor(() => { - expect(updateApiCalled).toBe(true); - expect(capturedRequestBody).toStrictEqual({ - timeRangeEnabled: true, - defaultTimeRange: DEFAULT_TIME_RANGE, - }); - expect(mockToast.success).toHaveBeenCalledWith( - 'Public dashboard updated successfully', - ); - }); - }); - }); - - describe('Revoke Public Dashboard Access', () => { - it('should call revoke API when unpublish button is clicked', async () => { - const user = userEvent.setup({ pointerEventsCheck: 0 }); - let revokeApiCalled = false; - let capturedDashboardId: string | null = null; - - server.use( - rest.get( - `*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`, - (_req, res, ctx) => - res(ctx.status(StatusCodes.OK), ctx.json(publishedPublicDashboardMeta)), - ), - rest.delete(publicDashboardURL, (req, res, ctx) => { - revokeApiCalled = true; - // Extract dashboard ID from URL: /api/v1/dashboards/{id}/public - const urlMatch = req.url.pathname.match( - /\/api\/v1\/dashboards\/([^/]+)\/public/, - ); - capturedDashboardId = urlMatch ? urlMatch[1] : null; - return res(ctx.status(StatusCodes.NO_CONTENT), ctx.json({})); - }), - ); - - render(); - - // Wait for API response and component update - const unpublishButton = await screen.findByRole( - 'button', - { name: /unpublish dashboard/i }, - { timeout: 5000 }, - ); - await user.click(unpublishButton); - - await waitFor(() => { - expect(revokeApiCalled).toBe(true); - expect(capturedDashboardId).toBe(MOCK_DASHBOARD_ID); - expect(mockToast.success).toHaveBeenCalledWith( - 'Dashboard unpublished successfully', - ); - }); - }); - }); - - describe('Non-admin user permissions', () => { - it('should disable "Publish dashboard" button for non-admin users', async () => { - server.use( - rest.get( - `*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`, - (_req, res, ctx) => - res( - ctx.status(StatusCodes.NOT_FOUND), - ctx.json(unpublishedPublicDashboardMeta), - ), - ), - ); - - render(, {}, { role: USER_ROLES.VIEWER }); - - const publishButton = await screen.findByRole('button', { - name: /publish dashboard/i, - }); - expect(publishButton).toBeInTheDocument(); - expect(publishButton).toBeDisabled(); - expect(publishButton).toHaveAttribute('disabled'); - }); - - describe('Published dashboard buttons for non-admin users', () => { - beforeEach(() => { - server.use( - rest.get( - `*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`, - (_req, res, ctx) => - res(ctx.status(StatusCodes.OK), ctx.json(publishedPublicDashboardMeta)), - ), - ); - }); - - it('should disable "Unpublish dashboard" button for non-admin users', async () => { - render(, {}, { role: USER_ROLES.VIEWER }); - - const unpublishButton = await screen.findByRole('button', { - name: /unpublish dashboard/i, - }); - expect(unpublishButton).toBeInTheDocument(); - expect(unpublishButton).toBeDisabled(); - expect(unpublishButton).toHaveAttribute('disabled'); - }); - - it('should disable "Update published dashboard" button for non-admin users', async () => { - render(, {}, { role: USER_ROLES.VIEWER }); - - const updateButton = await screen.findByRole('button', { - name: /update published dashboard/i, - }); - expect(updateButton).toBeInTheDocument(); - expect(updateButton).toBeDisabled(); - expect(updateButton).toHaveAttribute('disabled'); - }); - }); - }); -}); diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/index.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/index.tsx deleted file mode 100644 index d6eba8abaee..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/index.tsx +++ /dev/null @@ -1,378 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useMutation } from 'react-query'; -import { useCopyToClipboard } from 'react-use'; -import { Checkbox } from '@signozhq/ui/checkbox'; -import { toast } from '@signozhq/ui/sonner'; -import { Button, Select } from 'antd'; -import { Typography } from '@signozhq/ui/typography'; -import createPublicDashboardAPI from 'api/dashboard/public/createPublicDashboard'; -import revokePublicDashboardAccessAPI from 'api/dashboard/public/revokePublicDashboardAccess'; -import updatePublicDashboardAPI from 'api/dashboard/public/updatePublicDashboard'; -import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; -import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta'; -import { useGetTenantLicense } from 'hooks/useGetTenantLicense'; -import { - Copy, - ExternalLink, - Globe, - Info, - LoaderCircle, - Trash, -} from '@signozhq/icons'; -import { useAppContext } from 'providers/App/App'; -import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore'; -import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta'; -import APIError from 'types/api/error'; -import { USER_ROLES } from 'types/roles'; -import { getAbsoluteUrl } from 'utils/basePath'; -import { openInNewTab } from 'utils/navigation'; - -import './PublicDashboard.styles.scss'; - -export const TIME_RANGE_PRESETS_OPTIONS = [ - { - label: 'Last 5 minutes', - value: '5m', - }, - { - label: 'Last 15 minutes', - value: '15m', - }, - { - label: 'Last 30 minutes', - value: '30m', - }, - { - label: 'Last 1 hour', - value: '1h', - }, - { - label: 'Last 6 hours', - value: '6h', - }, - { - label: 'Last 1 day', - value: '24h', - }, -]; - -const showErrorNotification = (error: APIError): void => { - toast.error(error.getErrorCode(), { - description: error.getErrorMessage(), - }); -}; - -function PublicDashboardSetting(): JSX.Element { - const [publicDashboardData, setPublicDashboardData] = useState< - PublicDashboardMetaProps | undefined - >(undefined); - const [timeRangeEnabled, setTimeRangeEnabled] = useState(true); - const [defaultTimeRange, setDefaultTimeRange] = useState(DEFAULT_TIME_RANGE); - const [, setCopyPublicDashboardURL] = useCopyToClipboard(); - - const { dashboardData } = useDashboardStore(); - - const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense(); - - const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser; - - const { user } = useAppContext(); - - const isAdmin = user?.role === USER_ROLES.ADMIN; - - const handleDefaultTimeRange = useCallback((value: string): void => { - setDefaultTimeRange(value); - }, []); - - const handleTimeRangeEnabled = useCallback((): void => { - setTimeRangeEnabled((prev) => !prev); - }, []); - - const { - data: publicDashboardResponse, - isLoading: isLoadingPublicDashboard, - isFetching: isFetchingPublicDashboard, - refetch: refetchPublicDashboard, - error: errorPublicDashboard, - } = useGetPublicDashboardMeta( - dashboardData?.id || '', - !!dashboardData?.id && isPublicDashboardEnabled, - ); - - const isPublicDashboard = !!publicDashboardData?.publicPath; - - useEffect(() => { - if (publicDashboardResponse?.data) { - setPublicDashboardData(publicDashboardResponse?.data); - } - - if (errorPublicDashboard) { - console.error('Error getting public dashboard', errorPublicDashboard); - setPublicDashboardData(undefined); - setTimeRangeEnabled(true); - setDefaultTimeRange(DEFAULT_TIME_RANGE); - } - }, [publicDashboardResponse, errorPublicDashboard]); - - useEffect(() => { - if (publicDashboardResponse?.data) { - setTimeRangeEnabled( - publicDashboardResponse?.data?.timeRangeEnabled || false, - ); - setDefaultTimeRange( - publicDashboardResponse?.data?.defaultTimeRange || DEFAULT_TIME_RANGE, - ); - } - }, [publicDashboardResponse]); - - const { - mutate: createPublicDashboard, - isLoading: isLoadingCreatePublicDashboard, - data: createPublicDashboardResponse, - } = useMutation(createPublicDashboardAPI, { - onSuccess: () => { - toast.success('Public dashboard created successfully'); - }, - onError: (error: APIError) => { - showErrorNotification(error); - }, - }); - - const { - mutate: updatePublicDashboard, - isLoading: isLoadingUpdatePublicDashboard, - data: updatePublicDashboardResponse, - } = useMutation(updatePublicDashboardAPI, { - onSuccess: () => { - toast.success('Public dashboard updated successfully'); - }, - onError: (error: APIError) => { - showErrorNotification(error); - }, - }); - - const { - mutate: revokePublicDashboardAccess, - isLoading: isLoadingRevokePublicDashboardAccess, - data: revokePublicDashboardAccessResponse, - } = useMutation(revokePublicDashboardAccessAPI, { - onSuccess: () => { - toast.success('Dashboard unpublished successfully'); - }, - onError: (error: APIError) => { - showErrorNotification(error); - }, - }); - - const handleCreatePublicDashboard = (): void => { - if (!dashboardData) { - return; - } - - createPublicDashboard({ - dashboardId: dashboardData.id, - timeRangeEnabled, - defaultTimeRange, - }); - }; - - const handleUpdatePublicDashboard = (): void => { - if (!dashboardData) { - return; - } - - updatePublicDashboard({ - dashboardId: dashboardData.id, - timeRangeEnabled, - defaultTimeRange, - }); - }; - - const handleRevokePublicDashboardAccess = (): void => { - if (!dashboardData) { - return; - } - - revokePublicDashboardAccess({ - id: dashboardData.id, - }); - }; - - useEffect(() => { - if ( - (createPublicDashboardResponse && - createPublicDashboardResponse.httpStatusCode === 201) || - (updatePublicDashboardResponse && - updatePublicDashboardResponse.httpStatusCode === 204) || - (revokePublicDashboardAccessResponse && - revokePublicDashboardAccessResponse.httpStatusCode === 204) - ) { - refetchPublicDashboard(); - } - }, [ - createPublicDashboardResponse, - updatePublicDashboardResponse, - revokePublicDashboardAccessResponse, - refetchPublicDashboard, - ]); - - const handleCopyPublicDashboardURL = (): void => { - if (!publicDashboardResponse?.data?.publicPath) { - return; - } - - try { - setCopyPublicDashboardURL( - getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''), - ); - toast.success('Copied Public Dashboard URL successfully'); - } catch (error) { - console.error('Error copying public dashboard URL', error); - } - }; - - const publicDashboardURL = useMemo( - () => getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''), - [publicDashboardResponse], - ); - - const isLoading = - isLoadingCreatePublicDashboard || - isLoadingUpdatePublicDashboard || - isLoadingRevokePublicDashboardAccess || - isLoadingPublicDashboard; - - return ( -
-
- - {isPublicDashboard - ? 'This dashboard is publicly accessible. Anyone with the link can view it.' - : 'This dashboard is private. Publish it to make it accessible to anyone with the link.'} - - -
- - Enable time range - -
- -
-
- - Default time range - -
- - ); -} - -export default memo(TextboxVariableInput); diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/VariableItem.test.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/VariableItem.test.tsx deleted file mode 100644 index 633eae5b942..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/VariableItem.test.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import React, { useEffect } from 'react'; -import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; -import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -import '@testing-library/jest-dom/extend-expect'; - -import VariableItem from './VariableItem'; - -const mockVariableData: IDashboardVariable = { - id: 'test_variable', - description: 'Test Variable', - type: 'TEXTBOX', - textboxValue: 'defaultValue', - sort: 'DISABLED', - multiSelect: false, - showALLOption: false, - name: 'testVariable', -}; - -// New mock data for a custom variable -const mockCustomVariableData: IDashboardVariable = { - ...mockVariableData, - name: 'customVariable', - type: 'CUSTOM', - customValue: 'option1,option2,option3', -}; - -const mockOnValueUpdate = jest.fn(); - -describe('VariableItem', () => { - let useEffectSpy: jest.SpyInstance; - - beforeEach(() => { - useEffectSpy = jest.spyOn(React, 'useEffect'); - }); - - afterEach(() => { - jest.clearAllMocks(); - useEffectSpy.mockRestore(); - }); - - it('renders component with default props', () => { - render( - - - , - ); - - expect(screen.getByText('$testVariable')).toBeInTheDocument(); - }); - - it('renders Input when the variable type is TEXTBOX', () => { - render( - - - , - ); - expect( - screen.getByTestId('variable-textbox-test_variable'), - ).toBeInTheDocument(); - }); - - it('calls onValueUpdate when Input value changes and blurs', async () => { - render( - - - , - ); - - const inputElement = screen.getByTestId('variable-textbox-test_variable'); - - // Change the value - act(() => { - fireEvent.change(inputElement, { target: { value: 'newValue' } }); - }); - - // Blur the input to trigger the update - act(() => { - fireEvent.blur(inputElement); - }); - - await waitFor(() => { - expect(mockOnValueUpdate).toHaveBeenCalledWith( - 'testVariable', - 'test_variable', - 'newValue', - false, - ); - }); - }); - - it('renders a Select element when variable type is CUSTOM', () => { - render( - - - , - ); - - expect(screen.getByText('$customVariable')).toBeInTheDocument(); - expect(screen.getByTestId('variable-select')).toBeInTheDocument(); - }); - - it('renders a Select element with all selected', async () => { - const customVariableData = { - ...mockCustomVariableData, - allSelected: true, - showALLOption: true, - multiSelect: true, - }; - - render( - - - , - ); - - expect(screen.getByText('ALL')).toBeInTheDocument(); - }); - - it('calls useEffect when the component mounts', () => { - render( - - - , - ); - - expect(useEffect).toHaveBeenCalled(); - }); -}); diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/VariableItem.tsx deleted file mode 100644 index 9dc39aaf545..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/VariableItem.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { memo } from 'react'; -import { Tooltip } from 'antd'; -import { Typography } from '@signozhq/ui/typography'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; -import { SolidInfoCircle } from '@signozhq/icons'; - -import CustomVariableInput from './CustomVariableInput'; -import DynamicVariableInput from './DynamicVariableInput'; -import QueryVariableInput from './QueryVariableInput'; -import TextboxVariableInput from './TextboxVariableInput'; - -import './DashboardVariableSelection.styles.scss'; - -export interface VariableItemProps { - variableData: IDashboardVariable; - existingVariables: Record; - onValueUpdate: ( - name: string, - id: string, - value: IDashboardVariable['selectedValue'], - allSelected: boolean, - haveCustomValuesSelected?: boolean, - ) => void; -} - -function VariableItem({ - variableData, - onValueUpdate, - existingVariables, -}: VariableItemProps): JSX.Element { - const { name, description, type: variableType } = variableData; - - return ( -
- - ${name} - {description && ( - - - - )} - - -
- {variableType === 'TEXTBOX' && ( - - )} - {variableType === 'CUSTOM' && ( - - )} - {variableType === 'QUERY' && ( - - )} - {variableType === 'DYNAMIC' && ( - - )} -
-
- ); -} - -export default memo(VariableItem); diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/DashboardVariableSelection.test.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/DashboardVariableSelection.test.tsx deleted file mode 100644 index 8995ccbf2b3..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/DashboardVariableSelection.test.tsx +++ /dev/null @@ -1,388 +0,0 @@ -import { act, render } from '@testing-library/react'; -import * as dashboardVariablesStoreModule from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore'; -import { - dashboardVariablesStore, - setDashboardVariablesStore, - updateDashboardVariablesStore, -} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore'; -import { - IDashboardVariables, - IDashboardVariablesStoreState, -} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes'; -import { - enqueueDescendantsOfVariable, - enqueueFetchOfAllVariables, - initializeVariableFetchStore, -} from 'providers/Dashboard/store/variableFetchStore'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -import DashboardVariableSelection from '../DashboardVariableSelection'; - -// Mutable container to capture the onValueUpdate callback from VariableItem -const mockVariableItemCallbacks: { - onValueUpdate?: ( - name: string, - id: string, - value: IDashboardVariable['selectedValue'], - allSelected: boolean, - haveCustomValuesSelected?: boolean, - ) => void; -} = {}; - -// Mock providers/Dashboard/Dashboard -const mockSetDashboardData = jest.fn(); -const mockUpdateLocalStorageDashboardVariables = jest.fn(); -interface MockDashboardStoreState { - dashboardData?: { id: string }; - setDashboardData: typeof mockSetDashboardData; - updateLocalStorageDashboardVariables: typeof mockUpdateLocalStorageDashboardVariables; -} -jest.mock('providers/Dashboard/store/useDashboardStore', () => ({ - useDashboardStore: ( - selector?: (s: Record) => MockDashboardStoreState, - ): MockDashboardStoreState => { - const state = { - dashboardData: { id: 'dash-1' }, - setDashboardData: mockSetDashboardData, - updateLocalStorageDashboardVariables: - mockUpdateLocalStorageDashboardVariables, - }; - return selector ? selector(state) : state; - }, -})); - -// Mock hooks/dashboard/useVariablesFromUrl -const mockUpdateUrlVariable = jest.fn(); -const mockGetUrlVariables = jest.fn().mockReturnValue({}); -jest.mock('hooks/dashboard/useVariablesFromUrl', () => ({ - __esModule: true, - default: (): Record => ({ - updateUrlVariable: mockUpdateUrlVariable, - getUrlVariables: mockGetUrlVariables, - }), -})); - -// Mock variableFetchStore functions -jest.mock('providers/Dashboard/store/variableFetchStore', () => ({ - initializeVariableFetchStore: jest.fn(), - enqueueFetchOfAllVariables: jest.fn(), - enqueueDescendantsOfVariable: jest.fn(), -})); - -// Mock initializeDefaultVariables -jest.mock('providers/Dashboard/initializeDefaultVariables', () => ({ - initializeDefaultVariables: jest.fn(), -})); - -// Mock react-redux useSelector for globalTime -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }), -})); - -// VariableItem mock captures the onValueUpdate prop for use in onValueUpdate tests -jest.mock('../VariableItem', () => ({ - __esModule: true, - default: (props: any): JSX.Element => { - mockVariableItemCallbacks.onValueUpdate = props.onValueUpdate; - return
; - }, -})); - -function createVariable( - overrides: Partial = {}, -): IDashboardVariable { - return { - id: 'test-id', - name: 'test-var', - description: '', - type: 'QUERY', - sort: 'DISABLED', - showALLOption: false, - multiSelect: false, - order: 0, - ...overrides, - }; -} - -function resetStore(): void { - dashboardVariablesStore.set(() => ({ - dashboardId: '', - variables: {}, - sortedVariablesArray: [], - dependencyData: null, - variableTypes: {}, - dynamicVariableOrder: [], - })); -} - -describe('DashboardVariableSelection', () => { - beforeEach(() => { - resetStore(); - jest.clearAllMocks(); - }); - - it('should call initializeVariableFetchStore and enqueueFetchOfAllVariables on mount', () => { - const variables: IDashboardVariables = { - env: createVariable({ name: 'env', type: 'QUERY', order: 0 }), - }; - - setDashboardVariablesStore({ dashboardId: 'dash-1', variables }); - - render(); - - expect(initializeVariableFetchStore).toHaveBeenCalledWith(['env']); - expect(enqueueFetchOfAllVariables).toHaveBeenCalled(); - }); - - it('should re-trigger fetch cycle when dynamicVariableOrder changes', () => { - const variables: IDashboardVariables = { - env: createVariable({ name: 'env', type: 'QUERY', order: 0 }), - }; - - setDashboardVariablesStore({ dashboardId: 'dash-1', variables }); - - render(); - - // Clear mocks after initial render - (initializeVariableFetchStore as jest.Mock).mockClear(); - (enqueueFetchOfAllVariables as jest.Mock).mockClear(); - - // Add a DYNAMIC variable which changes dynamicVariableOrder - act(() => { - updateDashboardVariablesStore({ - dashboardId: 'dash-1', - variables: { - env: createVariable({ name: 'env', type: 'QUERY', order: 0 }), - dyn1: createVariable({ name: 'dyn1', type: 'DYNAMIC', order: 1 }), - }, - }); - }); - - expect(initializeVariableFetchStore).toHaveBeenCalledWith( - expect.arrayContaining(['env', 'dyn1']), - ); - expect(enqueueFetchOfAllVariables).toHaveBeenCalled(); - }); - - it('should re-trigger fetch cycle when a dynamic variable is removed', () => { - const variables: IDashboardVariables = { - env: createVariable({ name: 'env', type: 'QUERY', order: 0 }), - dyn1: createVariable({ name: 'dyn1', type: 'DYNAMIC', order: 1 }), - dyn2: createVariable({ name: 'dyn2', type: 'DYNAMIC', order: 2 }), - }; - - setDashboardVariablesStore({ dashboardId: 'dash-1', variables }); - - render(); - - (initializeVariableFetchStore as jest.Mock).mockClear(); - (enqueueFetchOfAllVariables as jest.Mock).mockClear(); - - // Remove dyn2, changing dynamicVariableOrder from ['dyn1','dyn2'] to ['dyn1'] - act(() => { - updateDashboardVariablesStore({ - dashboardId: 'dash-1', - variables: { - env: createVariable({ name: 'env', type: 'QUERY', order: 0 }), - dyn1: createVariable({ name: 'dyn1', type: 'DYNAMIC', order: 1 }), - }, - }); - }); - - expect(initializeVariableFetchStore).toHaveBeenCalledWith(['env', 'dyn1']); - expect(enqueueFetchOfAllVariables).toHaveBeenCalled(); - }); - - it('should NOT re-trigger fetch cycle when dynamicVariableOrder stays the same', () => { - const variables: IDashboardVariables = { - env: createVariable({ name: 'env', type: 'QUERY', order: 0 }), - dyn1: createVariable({ name: 'dyn1', type: 'DYNAMIC', order: 1 }), - }; - - setDashboardVariablesStore({ dashboardId: 'dash-1', variables }); - - render(); - - (initializeVariableFetchStore as jest.Mock).mockClear(); - (enqueueFetchOfAllVariables as jest.Mock).mockClear(); - - // Update a non-dynamic variable's selectedValue — dynamicVariableOrder unchanged - act(() => { - const snapshot = dashboardVariablesStore.getSnapshot(); - dashboardVariablesStore.set( - (): IDashboardVariablesStoreState => ({ - ...snapshot, - variables: { - ...snapshot.variables, - env: { - ...snapshot.variables.env, - selectedValue: 'production', - }, - }, - }), - ); - }); - - expect(initializeVariableFetchStore).not.toHaveBeenCalled(); - expect(enqueueFetchOfAllVariables).not.toHaveBeenCalled(); - }); - - describe('onValueUpdate', () => { - let updateStoreSpy: jest.SpyInstance; - - beforeEach(() => { - resetStore(); - jest.clearAllMocks(); - // Real implementation pass-through — we just want to observe calls - updateStoreSpy = jest.spyOn( - dashboardVariablesStoreModule, - 'updateDashboardVariablesStore', - ); - }); - - afterEach(() => { - updateStoreSpy.mockRestore(); - }); - - it('updates dashboardVariablesStore synchronously before enqueueDescendantsOfVariable', () => { - setDashboardVariablesStore({ - dashboardId: 'dash-1', - variables: { - env: createVariable({ name: 'env', id: 'env-id', order: 0 }), - }, - }); - - render(); - - const callOrder: string[] = []; - updateStoreSpy.mockImplementation(() => { - callOrder.push('updateDashboardVariablesStore'); - }); - (enqueueDescendantsOfVariable as jest.Mock).mockImplementation(() => { - callOrder.push('enqueueDescendantsOfVariable'); - }); - - act(() => { - mockVariableItemCallbacks.onValueUpdate?.( - 'env', - 'env-id', - 'production', - false, - ); - }); - - expect(callOrder).toStrictEqual([ - 'updateDashboardVariablesStore', - 'enqueueDescendantsOfVariable', - ]); - }); - - it('passes updated variable value to dashboardVariablesStore', () => { - setDashboardVariablesStore({ - dashboardId: 'dash-1', - variables: { - env: createVariable({ - name: 'env', - id: 'env-id', - order: 0, - selectedValue: 'staging', - }), - }, - }); - - render(); - - // Clear spy calls that happened during setup/render - updateStoreSpy.mockClear(); - - act(() => { - mockVariableItemCallbacks.onValueUpdate?.( - 'env', - 'env-id', - 'production', - false, - ); - }); - - expect(updateStoreSpy).toHaveBeenCalledWith( - expect.objectContaining({ - dashboardId: 'dash-1', - variables: expect.objectContaining({ - env: expect.objectContaining({ - selectedValue: 'production', - allSelected: false, - }), - }), - }), - ); - }); - - it('calls enqueueDescendantsOfVariable synchronously without a timer', () => { - jest.useFakeTimers(); - - setDashboardVariablesStore({ - dashboardId: 'dash-1', - variables: { - env: createVariable({ name: 'env', id: 'env-id', order: 0 }), - }, - }); - - render(); - - act(() => { - mockVariableItemCallbacks.onValueUpdate?.( - 'env', - 'env-id', - 'production', - false, - ); - }); - - // Must be called immediately — no timer advancement needed - expect(enqueueDescendantsOfVariable).toHaveBeenCalledWith('env'); - - jest.useRealTimers(); - }); - - it('propagates allSelected and haveCustomValuesSelected to the store', () => { - setDashboardVariablesStore({ - dashboardId: 'dash-1', - variables: { - env: createVariable({ - name: 'env', - id: 'env-id', - order: 0, - multiSelect: true, - showALLOption: true, - }), - }, - }); - - render(); - updateStoreSpy.mockClear(); - - act(() => { - mockVariableItemCallbacks.onValueUpdate?.( - 'env', - 'env-id', - ['production', 'staging'], - true, - false, - ); - }); - - expect(updateStoreSpy).toHaveBeenCalledWith( - expect.objectContaining({ - variables: expect.objectContaining({ - env: expect.objectContaining({ - selectedValue: ['production', 'staging'], - allSelected: true, - haveCustomValuesSelected: false, - }), - }), - }), - ); - }); - }); -}); diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/DynamicVariableInput.test.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/DynamicVariableInput.test.tsx deleted file mode 100644 index 02c7a1a8261..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/DynamicVariableInput.test.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import * as ReactQuery from 'react-query'; -// eslint-disable-next-line no-restricted-imports -import * as ReactRedux from 'react-redux'; -import { fireEvent, render, screen } from '@testing-library/react'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -import DynamicVariableInput from '../DynamicVariableInput'; - -// Mock useVariableFetchState to return "fetching" state so useQuery is enabled -jest.mock('hooks/dashboard/useVariableFetchState', () => ({ - useVariableFetchState: (): Record => ({ - variableFetchCycleId: 0, - variableFetchState: 'loading', - isVariableSettled: false, - isVariableFetching: true, - hasVariableFetchedOnce: false, - isVariableWaitingForDependencies: false, - variableDependencyWaitMessage: '', - }), -})); - -// Don't mock the components - use real ones - -// Mock for useQuery -const mockQueryResult = { - data: undefined, - error: null, - isError: false, - isIdle: false, - isLoading: false, - isPreviousData: false, - isSuccess: true, - status: 'success', - isFetched: true, - isFetchingNextPage: false, - isFetchingPreviousPage: false, - isPlaceholderData: false, - isPaused: false, - isRefetchError: false, - isRefetching: false, - isStale: false, - isLoadingError: false, - isFetching: false, - isFetchedAfterMount: true, - dataUpdatedAt: 0, - errorUpdatedAt: 0, - failureCount: 0, - refetch: jest.fn(), - remove: jest.fn(), - fetchNextPage: jest.fn(), - fetchPreviousPage: jest.fn(), - hasNextPage: false, - hasPreviousPage: false, -} as any; - -// Sample data for testing -const mockApiResponse = { - data: { - normalizedValues: ['frontend', 'backend', 'database'], - complete: true, - }, - httpStatusCode: 200, -}; - -// Mock scrollIntoView since it's not available in JSDOM -window.HTMLElement.prototype.scrollIntoView = jest.fn(); - -describe('DynamicVariableInput Component', () => { - const mockOnValueUpdate = jest.fn(); - - const mockDynamicVariableData: IDashboardVariable = { - id: 'var1', - name: 'service', - type: 'DYNAMIC', - dynamicVariablesAttribute: 'service.name', - dynamicVariablesSource: 'Traces', - selectedValue: 'frontend', - multiSelect: false, - showALLOption: false, - allSelected: false, - description: '', - sort: 'DISABLED', - }; - - const mockMultiSelectDynamicVariableData: IDashboardVariable = { - ...mockDynamicVariableData, - id: 'var2', - name: 'services', - multiSelect: true, - selectedValue: ['frontend', 'backend'], - showALLOption: true, - }; - - const mockExistingVariables: Record = { - var1: mockDynamicVariableData, - var2: mockMultiSelectDynamicVariableData, - }; - - beforeEach(() => { - jest.clearAllMocks(); - mockOnValueUpdate.mockClear(); - - // Mock useSelector - const useSelectorSpy = jest.spyOn(ReactRedux, 'useSelector'); - useSelectorSpy.mockReturnValue({ - minTime: '2023-01-01T00:00:00Z', - maxTime: '2023-01-02T00:00:00Z', - }); - - // Mock useQuery with success state - const useQuerySpy = jest.spyOn(ReactQuery, 'useQuery'); - useQuerySpy.mockReturnValue({ - ...mockQueryResult, - data: mockApiResponse, - isLoading: false, - error: null, - }); - }); - - it('renders with single select variable correctly', () => { - render( - , - ); - - // Verify the selected value is displayed - const selectedItem = screen.getByRole('combobox'); - expect(selectedItem).toBeInTheDocument(); - - // CustomSelect doesn't use the 'mode' attribute for single select - expect(selectedItem).not.toHaveAttribute('mode'); - }); - - it('renders with multi select variable correctly', () => { - // First set up allSelected to true to properly test the ALL display - const multiSelectWithAllSelected = { - ...mockMultiSelectDynamicVariableData, - allSelected: true, - }; - - render( - , - ); - - // In ALL selected mode, there should be an "ALL" text element - expect(screen.getByText('ALL')).toBeInTheDocument(); - }); - - it('shows loading state correctly', () => { - // Mock loading state - jest.spyOn(ReactQuery, 'useQuery').mockReturnValue({ - ...mockQueryResult, - data: null, - isLoading: true, - isFetching: true, - isSuccess: false, - status: 'loading', - }); - - render( - , - ); - - // Open dropdown to see loading text - const selectElement = screen.getByRole('combobox'); - fireEvent.mouseDown(selectElement); - - // The loading text should appear in the dropdown - expect(screen.getByText('Refreshing values...')).toBeInTheDocument(); - }); - - it('handles error state correctly', () => { - const errorMessage = 'Failed to fetch data'; - - // Mock error state - jest.spyOn(ReactQuery, 'useQuery').mockReturnValue({ - ...mockQueryResult, - data: null, - isLoading: false, - isSuccess: false, - isError: true, - status: 'error', - error: { message: errorMessage }, - }); - - render( - , - ); - - // For error states, we should check that error handling is in place - // Without opening the dropdown as the error message might be handled differently - expect(ReactQuery.useQuery).toHaveBeenCalled(); - // We don't need to check refetch as it might be called during component initialization - }); - - it('makes API call to fetch variable values', () => { - render( - , - ); - - // Verify the useQuery hook was called with expected parameters - expect(ReactQuery.useQuery).toHaveBeenCalledWith( - [ - 'DASHBOARD_BY_ID', - mockDynamicVariableData.name, - 'service:"frontend"|services:["frontend","backend"]', // The actual dynamicVariablesKey - '2023-01-01T00:00:00Z', // minTime from useSelector mock - '2023-01-02T00:00:00Z', // maxTime from useSelector mock - '', - 'Traces', - 'service.name', - 0, // variableFetchCycleId - ], - expect.objectContaining({ - enabled: true, // isVariableFetching is true from mock - queryFn: expect.any(Function), - onSuccess: expect.any(Function), - onError: expect.any(Function), - }), - ); - }); - - it('has the correct selected value', () => { - // Use a different variable configuration to test different behavior - const customVariable = { - ...mockDynamicVariableData, - id: 'custom1', - name: 'customService', - selectedValue: 'backend', - }; - - render( - , - ); - - // Find the selection item in the component using data-testid - const selectElement = screen.getByTestId('variable-select'); - expect(selectElement).toBeInTheDocument(); - - // Check that the selected value is displayed in the select element - expect(selectElement).toHaveTextContent('backend'); - }); -}); diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/QueryVariableInput.test.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/QueryVariableInput.test.tsx deleted file mode 100644 index 6fce9b0f287..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/QueryVariableInput.test.tsx +++ /dev/null @@ -1,274 +0,0 @@ -import { QueryClient, QueryClientProvider } from 'react-query'; -import { act, render, waitFor } from '@testing-library/react'; -import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery'; -import { variableFetchStore } from 'providers/Dashboard/store/variableFetchStore'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -import QueryVariableInput from '../QueryVariableInput'; - -jest.mock('api/dashboard/variables/dashboardVariablesQuery'); - -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }), -})); - -function createTestQueryClient(): QueryClient { - return new QueryClient({ - defaultOptions: { - queries: { retry: false, refetchOnWindowFocus: false }, - }, - }); -} - -function Wrapper({ - children, - queryClient, -}: { - children: React.ReactNode; - queryClient: QueryClient; -}): JSX.Element { - return ( - {children} - ); -} - -function createVariable( - overrides: Partial = {}, -): IDashboardVariable { - return { - id: 'env-id', - name: 'env', - description: '', - type: 'QUERY', - sort: 'DISABLED', - showALLOption: false, - multiSelect: false, - order: 0, - queryValue: 'SELECT env FROM table', - ...overrides, - }; -} - -/** Put the named variable into 'loading' state so useQuery fires on mount */ -function setVariableLoading(name: string): void { - variableFetchStore.update((draft) => { - draft.states[name] = 'loading'; - draft.cycleIds[name] = (draft.cycleIds[name] || 0) + 1; - }); -} - -function resetFetchStore(): void { - variableFetchStore.set(() => ({ - states: {}, - lastUpdated: {}, - cycleIds: {}, - })); -} - -describe('QueryVariableInput - getOptions logic', () => { - const mockOnValueUpdate = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - resetFetchStore(); - }); - - afterEach(() => { - resetFetchStore(); - }); - - it('applies default value (first option) when selectedValue is empty on first load', async () => { - (dashboardVariablesQuery as jest.Mock).mockResolvedValue({ - statusCode: 200, - payload: { variableValues: ['production', 'staging', 'dev'] }, - }); - - const variable = createVariable({ selectedValue: undefined }); - setVariableLoading('env'); - - const queryClient = createTestQueryClient(); - render( - - - , - ); - - await waitFor(() => { - expect(mockOnValueUpdate).toHaveBeenCalledWith( - 'env', - 'env-id', - 'production', // first option by default - false, - ); - }); - }); - - it('keeps existing selectedValue when it is present in new options', async () => { - (dashboardVariablesQuery as jest.Mock).mockResolvedValue({ - statusCode: 200, - payload: { variableValues: ['production', 'staging'] }, - }); - - const variable = createVariable({ selectedValue: 'staging' }); - setVariableLoading('env'); - - const queryClient = createTestQueryClient(); - render( - - - , - ); - - await waitFor(() => { - expect(mockOnValueUpdate).toHaveBeenCalledWith( - 'env', - 'env-id', - 'staging', - false, - ); - }); - }); - - it('selects all new options when allSelected=true and value is missing from new options', async () => { - (dashboardVariablesQuery as jest.Mock).mockResolvedValue({ - statusCode: 200, - payload: { variableValues: ['production', 'staging'] }, - }); - - const variable = createVariable({ - selectedValue: ['old-env'], - allSelected: true, - multiSelect: true, - showALLOption: true, - }); - setVariableLoading('env'); - - const queryClient = createTestQueryClient(); - render( - - - , - ); - - await waitFor(() => { - expect(mockOnValueUpdate).toHaveBeenCalledWith( - 'env', - 'env-id', - ['production', 'staging'], - true, - ); - }); - }); - - it('does not call onValueUpdate a second time when options have not changed', async () => { - const mockQueryFn = jest.fn().mockResolvedValue({ - statusCode: 200, - payload: { variableValues: ['production', 'staging'] }, - }); - (dashboardVariablesQuery as jest.Mock).mockImplementation(mockQueryFn); - - const variable = createVariable({ selectedValue: 'production' }); - setVariableLoading('env'); - - const queryClient = createTestQueryClient(); - render( - - - , - ); - - // Wait for first fetch and onValueUpdate call - await waitFor(() => { - expect(mockOnValueUpdate).toHaveBeenCalledTimes(1); - }); - - mockOnValueUpdate.mockClear(); - - // Trigger a second fetch cycle with the same API response - act(() => { - variableFetchStore.update((draft) => { - draft.states['env'] = 'revalidating'; - draft.cycleIds['env'] = (draft.cycleIds['env'] || 0) + 1; - }); - }); - - // Wait for second query to fire - await waitFor(() => { - expect(mockQueryFn).toHaveBeenCalledTimes(2); - }); - - // Options are unchanged, so onValueUpdate must not fire again - expect(mockOnValueUpdate).not.toHaveBeenCalled(); - }); - - it('does not call onValueUpdate when API returns a non-array response', async () => { - (dashboardVariablesQuery as jest.Mock).mockResolvedValue({ - statusCode: 200, - payload: { variableValues: null }, - }); - - const variable = createVariable({ selectedValue: 'production' }); - setVariableLoading('env'); - - const queryClient = createTestQueryClient(); - render( - - - , - ); - - await waitFor(() => { - expect(dashboardVariablesQuery).toHaveBeenCalled(); - }); - - expect(mockOnValueUpdate).not.toHaveBeenCalled(); - }); - - it('does not fire the query when variableData.name is empty', () => { - (dashboardVariablesQuery as jest.Mock).mockResolvedValue({ - statusCode: 200, - payload: { variableValues: ['production'] }, - }); - - // Variable with no name — useVariableFetchState will be called with '' - // and the query key will have an empty name, leaving it disabled - const variable = createVariable({ name: '' }); - // Note: we do NOT put it in 'loading' state since name is empty - // (no variableFetchStore entry for '' means isVariableFetching=false) - - const queryClient = createTestQueryClient(); - render( - - - , - ); - - expect(dashboardVariablesQuery).not.toHaveBeenCalled(); - expect(mockOnValueUpdate).not.toHaveBeenCalled(); - }); -}); diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/VariableItem.defaulting.behavior.test.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/VariableItem.defaulting.behavior.test.tsx deleted file mode 100644 index 9adb3642798..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/VariableItem.defaulting.behavior.test.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; -import { render, screen, waitFor } from 'tests/test-utils'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -import '@testing-library/jest-dom/extend-expect'; - -import VariableItem from '../VariableItem'; - -const mockOnValueUpdate = jest.fn(); - -const TEST_VARIABLE_ID = 'test_variable'; -const VARIABLE_SELECT_TESTID = 'variable-select'; -const TEST_VARIABLE_NAME = 'testVariable'; -const TEST_VARIABLE_DESCRIPTION = 'Test Variable'; - -const renderVariableItem = ( - variableData: IDashboardVariable, -): ReturnType => - render( - - - , - ); - -describe('VariableItem Default Value Selection Behavior', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Single Select Variables', () => { - it('should keep previous selection value', async () => { - const variable: IDashboardVariable = { - id: TEST_VARIABLE_ID, - name: TEST_VARIABLE_NAME, - description: TEST_VARIABLE_DESCRIPTION, - type: 'CUSTOM', - customValue: 'option1,option2,option3', - selectedValue: 'option1', - sort: 'DISABLED', - multiSelect: false, - showALLOption: false, - }; - - renderVariableItem(variable); - - await waitFor(() => { - expect(screen.getByTestId(VARIABLE_SELECT_TESTID)).toBeInTheDocument(); - }); - - await expect(screen.findByText('option1')).resolves.toBeInTheDocument(); - }); - - it('should auto-select first option when no previous and no default', async () => { - const variable: IDashboardVariable = { - id: TEST_VARIABLE_ID, - name: TEST_VARIABLE_NAME, - description: TEST_VARIABLE_DESCRIPTION, - type: 'CUSTOM', - customValue: 'option1,option2,option3', - selectedValue: undefined, - sort: 'DISABLED', - multiSelect: false, - showALLOption: false, - }; - - renderVariableItem(variable); - - await waitFor(() => { - expect(screen.getByTestId(VARIABLE_SELECT_TESTID)).toBeInTheDocument(); - }); - - // With the new variable select strategy, the first option is auto-selected - await expect(screen.findByText('option1')).resolves.toBeInTheDocument(); - }); - }); - - describe('Multi Select Variables with ALL enabled', () => { - it('should show ALL when all options are selected', async () => { - const variable: IDashboardVariable = { - id: TEST_VARIABLE_ID, - name: TEST_VARIABLE_NAME, - description: TEST_VARIABLE_DESCRIPTION, - type: 'CUSTOM', - customValue: 'option1,option2,option3', - selectedValue: ['option1', 'option2', 'option3'], - allSelected: true, - sort: 'DISABLED', - multiSelect: true, - showALLOption: true, - }; - - renderVariableItem(variable); - - await waitFor(() => { - expect(screen.getByTestId(VARIABLE_SELECT_TESTID)).toBeInTheDocument(); - }); - - await expect(screen.findByText('ALL')).resolves.toBeInTheDocument(); - }); - }); - - describe('Multi Select Variables with ALL disabled', () => { - it('should show placeholder when no selection', async () => { - const variable: IDashboardVariable = { - id: TEST_VARIABLE_ID, - name: TEST_VARIABLE_NAME, - description: TEST_VARIABLE_DESCRIPTION, - type: 'CUSTOM', - customValue: 'option1,option2,option3', - selectedValue: undefined, - sort: 'DISABLED', - multiSelect: true, - showALLOption: false, - }; - - renderVariableItem(variable); - - await waitFor(() => { - expect(screen.getByTestId(VARIABLE_SELECT_TESTID)).toBeInTheDocument(); - }); - - await expect(screen.findByText('Select value')).resolves.toBeInTheDocument(); - }); - }); -}); diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/dashboardVariables.test.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/dashboardVariables.test.tsx deleted file mode 100644 index c84f362b145..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/dashboardVariables.test.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { - buildDependencies, - buildDependencyGraph, - buildParentDependencyGraph, - onUpdateVariableNode, - VariableGraph, -} from '../util'; -import { - buildDependenciesMock, - buildGraphMock, - onUpdateVariableNodeMock, -} from './mock'; - -describe('dashboardVariables - utilities and processors', () => { - describe('onUpdateVariableNode', () => { - const { graph, topologicalOrder } = onUpdateVariableNodeMock; - const testCases = [ - { - scenario: 'root element', - nodeToUpdate: 'deployment_environment', - expected: [ - 'deployment_environment', - 'service_name', - 'endpoint', - 'http_status_code', - ], - }, - { - scenario: 'middle child', - nodeToUpdate: 'k8s_node_name', - expected: ['k8s_node_name', 'k8s_namespace_name'], - }, - { - scenario: 'leaf element', - nodeToUpdate: 'http_status_code', - expected: ['http_status_code'], - }, - { - scenario: 'node not in graph', - nodeToUpdate: 'unknown', - expected: [], - }, - { - scenario: 'node not in topological order', - nodeToUpdate: 'unknown', - expected: [], - }, - ]; - - it.each(testCases)( - 'should update variable node when $scenario', - ({ nodeToUpdate, expected }) => { - const updatedVariables: string[] = []; - const callback = (node: string): void => { - updatedVariables.push(node); - }; - - onUpdateVariableNode(nodeToUpdate, graph, topologicalOrder, callback); - - expect(updatedVariables).toStrictEqual(expected); - }, - ); - - it('should return empty array when topological order is empty', () => { - const updatedVariables: string[] = []; - onUpdateVariableNode('http_status_code', graph, [], (node) => - updatedVariables.push(node), - ); - expect(updatedVariables).toStrictEqual([]); - }); - }); - - describe('Graph Building Utilities', () => { - const { graph } = buildGraphMock; - const { variables } = buildDependenciesMock; - - describe('buildParentDependencyGraph', () => { - it('should build parent dependency graph with correct relationships', () => { - const expected = { - deployment_environment: [], - service_name: ['deployment_environment'], - endpoint: ['deployment_environment', 'service_name'], - http_status_code: ['endpoint'], - k8s_cluster_name: [], - k8s_node_name: ['k8s_cluster_name'], - k8s_namespace_name: ['k8s_cluster_name', 'k8s_node_name'], - environment: [], - }; - - expect(buildParentDependencyGraph(graph)).toStrictEqual(expected); - }); - - it('should handle empty graph', () => { - expect(buildParentDependencyGraph({})).toStrictEqual({}); - }); - }); - - describe('buildDependencyGraph', () => { - it('should build complete dependency graph with correct structure and order', () => { - const expected = { - order: [ - 'deployment_environment', - 'k8s_cluster_name', - 'environment', - 'service_name', - 'k8s_node_name', - 'endpoint', - 'k8s_namespace_name', - 'http_status_code', - ], - graph: { - deployment_environment: ['service_name', 'endpoint'], - service_name: ['endpoint'], - endpoint: ['http_status_code'], - http_status_code: [], - k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'], - k8s_node_name: ['k8s_namespace_name'], - k8s_namespace_name: [], - environment: [], - }, - parentDependencyGraph: { - deployment_environment: [], - service_name: ['deployment_environment'], - endpoint: ['deployment_environment', 'service_name'], - http_status_code: ['endpoint'], - k8s_cluster_name: [], - k8s_node_name: ['k8s_cluster_name'], - k8s_namespace_name: ['k8s_cluster_name', 'k8s_node_name'], - environment: [], - }, - hasCycle: false, - cycleNodes: undefined, - transitiveDescendants: { - deployment_environment: ['service_name', 'endpoint', 'http_status_code'], - endpoint: ['http_status_code'], - environment: [], - http_status_code: [], - k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'], - k8s_namespace_name: [], - k8s_node_name: ['k8s_namespace_name'], - service_name: ['endpoint', 'http_status_code'], - }, - }; - - expect(buildDependencyGraph(graph)).toStrictEqual(expected); - }); - - it('should return empty transitiveDescendants for an empty graph', () => { - const result = buildDependencyGraph({}); - expect(result.transitiveDescendants).toStrictEqual({}); - expect(result.order).toStrictEqual([]); - expect(result.hasCycle).toBe(false); - }); - - it('should compute transitiveDescendants for a linear chain (a -> b -> c)', () => { - const linearGraph: VariableGraph = { - a: ['b'], - b: ['c'], - c: [], - }; - const result = buildDependencyGraph(linearGraph); - expect(result.transitiveDescendants).toStrictEqual({ - a: ['b', 'c'], - b: ['c'], - c: [], - }); - }); - - it('should compute transitiveDescendants for a diamond dependency (a -> b, a -> c, b -> d, c -> d)', () => { - const diamondGraph: VariableGraph = { - a: ['b', 'c'], - b: ['d'], - c: ['d'], - d: [], - }; - const result = buildDependencyGraph(diamondGraph); - expect(result.transitiveDescendants.a).toStrictEqual( - expect.arrayContaining(['b', 'c', 'd']), - ); - expect(result.transitiveDescendants.a).toHaveLength(3); - expect(result.transitiveDescendants.b).toStrictEqual(['d']); - expect(result.transitiveDescendants.c).toStrictEqual(['d']); - expect(result.transitiveDescendants.d).toStrictEqual([]); - }); - - it('should handle disconnected components in transitiveDescendants', () => { - const disconnectedGraph: VariableGraph = { - a: ['b'], - b: [], - x: ['y'], - y: [], - }; - const result = buildDependencyGraph(disconnectedGraph); - expect(result.transitiveDescendants.a).toStrictEqual(['b']); - expect(result.transitiveDescendants.b).toStrictEqual([]); - expect(result.transitiveDescendants.x).toStrictEqual(['y']); - expect(result.transitiveDescendants.y).toStrictEqual([]); - }); - - it('should return empty transitiveDescendants for all leaf nodes', () => { - const leafOnlyGraph: VariableGraph = { - a: [], - b: [], - c: [], - }; - const result = buildDependencyGraph(leafOnlyGraph); - expect(result.transitiveDescendants).toStrictEqual({ - a: [], - b: [], - c: [], - }); - }); - }); - - describe('buildDependencies', () => { - it('should build dependency map from variables array', () => { - const expected: VariableGraph = { - deployment_environment: ['service_name', 'endpoint'], - service_name: ['endpoint'], - endpoint: ['http_status_code'], - http_status_code: [], - k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'], - k8s_node_name: ['k8s_namespace_name'], - k8s_namespace_name: [], - environment: [], - }; - - expect(buildDependencies(variables)).toStrictEqual(expected); - }); - - it('should handle empty variables array', () => { - expect(buildDependencies([])).toStrictEqual({}); - }); - }); - }); -}); diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/mock.ts b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/mock.ts deleted file mode 100644 index 3ba37f7b8c9..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/__test__/mock.ts +++ /dev/null @@ -1,218 +0,0 @@ -export const onUpdateVariableNodeMock = { - nodeToUpdate: 'deployment_environment', - graph: { - deployment_environment: ['service_name', 'endpoint'], - service_name: ['endpoint'], - endpoint: ['http_status_code'], - http_status_code: [], - k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'], - environment: [], - k8s_node_name: ['k8s_namespace_name'], - k8s_namespace_name: [], - }, - topologicalOrder: [ - 'deployment_environment', - 'k8s_cluster_name', - 'environment', - 'service_name', - 'k8s_node_name', - 'endpoint', - 'k8s_namespace_name', - 'http_status_code', - ], - callback: jest.fn(), -}; - -export const buildGraphMock = { - graph: { - deployment_environment: ['service_name', 'endpoint'], - service_name: ['endpoint'], - endpoint: ['http_status_code'], - http_status_code: [], - k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'], - environment: [], - k8s_node_name: ['k8s_namespace_name'], - k8s_namespace_name: [], - }, -}; - -export const buildDependenciesMock = { - variables: [ - { - key: '036a47cd-9ffc-47de-9f27-0329198964a8', - name: 'deployment_environment', - allSelected: false, - customValue: '', - description: '', - id: '036a47cd-9ffc-47de-9f27-0329198964a8', - modificationUUID: '5f71b591-f583-497c-839d-6a1590c3f60f', - multiSelect: false, - order: 0, - queryValue: - "SELECT DISTINCT JSONExtractString(labels, 'deployment_environment') AS deployment_environment\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'signoz_calls_total'", - selectedValue: 'production', - showALLOption: false, - sort: 'DISABLED', - textboxValue: '', - type: 'QUERY', - }, - { - key: 'eed5c917-1860-4c7e-bf6d-a05b97bafbc9', - name: 'service_name', - allSelected: true, - customValue: '', - description: '', - id: 'eed5c917-1860-4c7e-bf6d-a05b97bafbc9', - modificationUUID: '85db928b-ac9b-4e9f-b274-791112102fdf', - multiSelect: true, - order: 1, - queryValue: - "SELECT DISTINCT JSONExtractString(labels, 'service_name') FROM signoz_metrics.distributed_time_series_v4_1day\n WHERE metric_name = 'signoz_calls_total' and JSONExtractString(labels, 'deployment_environment') = {{.deployment_environment}}", - selectedValue: ['otelgateway'], - showALLOption: true, - sort: 'ASC', - textboxValue: '', - type: 'QUERY', - }, - { - key: '4022d3c1-e845-4952-8984-78f25f575c7a', - name: 'endpoint', - allSelected: true, - customValue: '', - description: '', - id: '4022d3c1-e845-4952-8984-78f25f575c7a', - modificationUUID: 'c0107fa1-ebb7-4dd3-aa9d-6ba08ecc594d', - multiSelect: true, - order: 2, - queryValue: - "SELECT DISTINCT JSONExtractString(labels, 'operation') FROM signoz_metrics.distributed_time_series_v4_1day\n WHERE metric_name = 'signoz_calls_total' AND JSONExtractString(labels, 'service_name') IN {{.service_name}} and JSONExtractString(labels, 'deployment_environment') = {{.deployment_environment}}", - selectedValue: [ - '//v1/traces', - '/logs/heroku', - '/logs/json', - '/logs/vector', - '/v1/logs', - '/v1/metrics', - '/v1/traces', - 'SELECT', - 'exporter/signozkafka/logs', - 'exporter/signozkafka/metrics', - 'exporter/signozkafka/traces', - 'extension/signozkeyauth/Authenticate', - 'get', - 'hmget', - 'opentelemetry.proto.collector.logs.v1.LogsService/Export', - 'opentelemetry.proto.collector.metrics.v1.MetricsService/Export', - 'opentelemetry.proto.collector.trace.v1.TraceService/Export', - 'processor/signozlimiter/LogsProcessed', - 'processor/signozlimiter/MetricsProcessed', - 'processor/signozlimiter/TracesProcessed', - 'receiver/otlp/LogsReceived', - 'receiver/otlp/MetricsReceived', - 'receiver/otlp/TraceDataReceived', - 'receiver/signozhttplog/heroku/LogsReceived', - 'receiver/signozhttplog/json/LogsReceived', - 'receiver/signozhttplog/vector/LogsReceived', - 'redis.dial', - 'redis.pipeline eval', - 'sadd', - 'set', - 'sismember', - ], - showALLOption: true, - sort: 'ASC', - textboxValue: '', - type: 'QUERY', - }, - { - key: '5e8a3cd9-3cd9-42df-a76c-79471a0f75bd', - name: 'http_status_code', - customValue: '', - description: '', - id: '5e8a3cd9-3cd9-42df-a76c-79471a0f75bd', - modificationUUID: '9a4021cc-a80a-4f15-8899-78892b763ca7', - multiSelect: true, - order: 3, - queryValue: - "SELECT DISTINCT JSONExtractString(labels, 'http_status_code') FROM signoz_metrics.distributed_time_series_v4_1day\n WHERE metric_name = 'signoz_calls_total' AND JSONExtractString(labels, 'operation') IN {{.endpoint}}", - showALLOption: true, - sort: 'ASC', - textboxValue: '', - type: 'QUERY', - selectedValue: ['', '200', '301', '400', '401', '405', '415', '429'], - allSelected: true, - }, - { - key: '48e9aa64-05ca-41c2-a1bd-6c8aeca659f1', - name: 'k8s_cluster_name', - allSelected: false, - customValue: 'test-1,\ntest-2,\ntest-3', - description: '', - id: '48e9aa64-05ca-41c2-a1bd-6c8aeca659f1', - modificationUUID: '44722322-368c-4613-bb7f-d0b12867d57a', - multiSelect: false, - order: 4, - queryValue: - "SELECT JSONExtractString(labels, 'k8s_cluster_name') AS k8s_cluster_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'k8s_node_cpu_time'\nGROUP BY k8s_cluster_name", - selectedValue: 'saasmonitor-cluster', - showALLOption: false, - sort: 'DISABLED', - textboxValue: '', - type: 'QUERY', - }, - { - key: '3ea18ba2-30cf-4220-b03b-720b5eaf35f8', - name: 'environment', - allSelected: false, - customValue: '', - description: '', - id: '3ea18ba2-30cf-4220-b03b-720b5eaf35f8', - modificationUUID: '9f76cb06-1b9f-460f-a174-0b210bb3cf93', - multiSelect: false, - order: 5, - queryValue: - "SELECT DISTINCT JSONExtractString(labels, 'deployment_environment') AS environment\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'signoz_calls_total'", - selectedValue: 'production', - showALLOption: false, - sort: 'DISABLED', - textboxValue: '', - type: 'QUERY', - }, - { - key: '4d71d385-beaf-4434-8dbf-c62be68049fc', - name: 'k8s_node_name', - allSelected: false, - customValue: '', - description: '', - id: '4d71d385-beaf-4434-8dbf-c62be68049fc', - modificationUUID: '77233d3c-96d7-4ccb-aa9d-11b04d563068', - multiSelect: false, - order: 6, - queryValue: - "SELECT JSONExtractString(labels, 'k8s_node_name') AS k8s_node_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'k8s_node_cpu_time' AND JSONExtractString(labels, 'k8s_cluster_name') = {{.k8s_cluster_name}}\nGROUP BY k8s_node_name", - selectedValue: 'gke-signoz-saas-si-consumer-bsc-e2sd4-a6d430fa-gvm2', - showALLOption: false, - sort: 'DISABLED', - textboxValue: '', - type: 'QUERY', - }, - { - key: '937ecbae-b24b-4d6d-8cc4-5d5b8d53569b', - name: 'k8s_namespace_name', - customValue: '', - description: '', - id: '937ecbae-b24b-4d6d-8cc4-5d5b8d53569b', - modificationUUID: '8ad2442d-8b4d-4c64-848e-af847d1d0eec', - multiSelect: false, - order: 7, - queryValue: - "SELECT JSONExtractString(labels, 'k8s_namespace_name') AS k8s_namespace_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'k8s_pod_cpu_time' AND JSONExtractString(labels, 'k8s_cluster_name') = {{.k8s_cluster_name}} AND JSONExtractString(labels, 'k8s_node_name') IN {{.k8s_node_name}}\nGROUP BY k8s_namespace_name", - showALLOption: false, - sort: 'DISABLED', - textboxValue: '', - type: 'QUERY', - selectedValue: 'saasmonitor', - allSelected: false, - }, - ] as any, -}; diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/index.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/index.tsx deleted file mode 100644 index 5b8e9e48c6f..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import DashboardVariableSelection from './DashboardVariableSelection'; - -export default DashboardVariableSelection; diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/styles.ts b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/styles.ts deleted file mode 100644 index 2f99160e08e..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/styles.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { grey } from '@ant-design/colors'; -import { Typography } from '@signozhq/ui/typography'; -import styled from 'styled-components'; - -export const VariableContainer = styled.div` - max-width: 100%; - border: 1px solid ${grey[1]}66; - border-radius: 2px; - padding: 0; - padding-left: 0.5rem; - margin-right: 8px; - display: flex; - align-items: center; - margin-bottom: 0.3rem; - gap: 4px; - padding: 4px; -`; - -export const VariableName = styled(Typography)` - font-size: 0.8rem; - color: ${grey[0]}; - - min-width: 100px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; -`; - -export const VariableValue = styled(Typography)` - font-size: 0.8rem; - color: ${grey[0]}; - - flex: 1; - - display: flex; - justify-content: flex-end; - align-items: center; - max-width: 300px; -`; - -export const SelectItemStyle = { - minWidth: 120, - fontSize: '0.8rem', - width: '100%', -}; diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/useDashboardVariableSelectHelper.ts b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/useDashboardVariableSelectHelper.ts deleted file mode 100644 index aea99642388..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/useDashboardVariableSelectHelper.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { useCallback, useMemo, useState } from 'react'; -import { isEmpty } from 'lodash-es'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -import { ALL_SELECT_VALUE } from '../utils'; -import { areArraysEqual, getSelectValue } from './util'; -import { VariableSelectStrategy } from './variableSelectStrategy/variableSelectStrategyTypes'; -import { - areArraysEqualIgnoreOrder, - uniqueValues, -} from './variableSelectStrategy/variableSelectStrategyUtils'; - -interface UseDashboardVariableSelectHelperParams { - variableData: IDashboardVariable; - optionsData: (string | number | boolean)[]; - onValueUpdate: ( - name: string, - id: string, - value: IDashboardVariable['selectedValue'], - allSelected: boolean, - haveCustomValuesSelected?: boolean, - ) => void; - strategy: VariableSelectStrategy; - /** Override for all available option strings (default: optionsData.map(String)) */ - allAvailableOptionStrings?: string[]; -} - -interface UseDashboardVariableSelectHelperReturn { - // State - tempSelection: string | string[] | undefined; - setTempSelection: React.Dispatch< - React.SetStateAction - >; - value: string | string[] | undefined; - defaultValue: string | string[] | undefined; - - // Derived values - enableSelectAll: boolean; - - // Handlers - onChange: (value: string | string[]) => void; - onDropdownVisibleChange: (visible: boolean) => void; - handleClear: () => void; - - // Default value helpers - applyDefaultIfNeeded: ( - overrideOptions?: (string | number | boolean)[], - ) => void; - getDefaultValue: ( - overrideOptions?: (string | number | boolean)[], - ) => string | string[] | undefined; -} - -// eslint-disable-next-line sonarjs/cognitive-complexity -export function useDashboardVariableSelectHelper({ - variableData, - optionsData, - onValueUpdate, - strategy, - allAvailableOptionStrings, -}: UseDashboardVariableSelectHelperParams): UseDashboardVariableSelectHelperReturn { - const { selectedValue } = variableData; - - const [tempSelection, setTempSelection] = useState< - string | string[] | undefined - >(undefined); - - const selectedValueStringified = useMemo( - () => getSelectValue(selectedValue, variableData), - [selectedValue, variableData], - ); - - const enableSelectAll = variableData.multiSelect && variableData.showALLOption; - - const effectiveAllAvailableOptionStrings = useMemo( - () => allAvailableOptionStrings ?? optionsData.map((v) => v.toString()), - [allAvailableOptionStrings, optionsData], - ); - - const selectValue = - variableData.allSelected && enableSelectAll - ? ALL_SELECT_VALUE - : selectedValueStringified; - - const getDefaultValue = useCallback( - (overrideOptions?: (string | number | boolean)[]) => { - const options = overrideOptions || optionsData; - if (variableData.multiSelect) { - if (variableData.showALLOption) { - return variableData.defaultValue || options.map((o) => o.toString()); - } - return variableData.defaultValue || options?.[0]?.toString(); - } - return variableData.defaultValue || options[0]?.toString(); - }, - [ - variableData.multiSelect, - variableData.showALLOption, - variableData.defaultValue, - optionsData, - ], - ); - - const defaultValue = useMemo(() => getDefaultValue(), [getDefaultValue]); - - const handleChange = useCallback( - (inputValue: string | string[]): void => { - const value = variableData.multiSelect && !inputValue ? [] : inputValue; - - if ( - value === variableData.selectedValue || - (Array.isArray(value) && - Array.isArray(variableData.selectedValue) && - areArraysEqual(value, variableData.selectedValue)) - ) { - return; - } - - strategy.handleChange({ - value, - variableData, - optionsData, - allAvailableOptionStrings: effectiveAllAvailableOptionStrings, - onValueUpdate, - }); - }, - [ - variableData, - optionsData, - effectiveAllAvailableOptionStrings, - onValueUpdate, - strategy, - ], - ); - - const handleTempChange = useCallback( - (inputValue: string | string[]): void => { - // Store the selection in temporary state while dropdown is open - const value = variableData.multiSelect && !inputValue ? [] : inputValue; - setTempSelection(uniqueValues(value)); - }, - [variableData.multiSelect], - ); - - // Single select onChange: apply default if value is empty - const handleSingleSelectChange = useCallback( - (inputValue: string | string[]): void => { - if (isEmpty(inputValue)) { - if (defaultValue !== undefined) { - handleChange(defaultValue as string | string[]); - } - return; - } - handleChange(inputValue); - }, - [handleChange, defaultValue], - ); - - // Handle dropdown visibility changes - const onDropdownVisibleChange = useCallback( - (visible: boolean): void => { - // Initialize temp selection when opening dropdown - if (visible) { - if (variableData.allSelected && enableSelectAll) { - // When ALL is selected, show all available options as individually checked - setTempSelection([...effectiveAllAvailableOptionStrings]); - } else { - setTempSelection(getSelectValue(variableData.selectedValue, variableData)); - } - } - // Apply changes when closing dropdown - else if (!visible && tempSelection !== undefined) { - // If ALL was selected before AND all options remain selected, skip updating - const wasAllSelected = enableSelectAll && variableData.allSelected; - const isAllSelectedAfter = - enableSelectAll && - Array.isArray(tempSelection) && - tempSelection.length === effectiveAllAvailableOptionStrings.length && - effectiveAllAvailableOptionStrings.every((v) => tempSelection.includes(v)); - - if (wasAllSelected && isAllSelectedAfter) { - setTempSelection(undefined); - return; - } - - // Apply default if closing with empty selection - let valueToApply = tempSelection; - if (isEmpty(tempSelection) && defaultValue !== undefined) { - valueToApply = defaultValue as string | string[]; - } - - // Order-agnostic change detection - const currentValue = variableData.selectedValue; - const hasChanged = - valueToApply !== currentValue && - !( - Array.isArray(valueToApply) && - Array.isArray(currentValue) && - areArraysEqualIgnoreOrder(valueToApply, currentValue) - ); - - if (hasChanged) { - handleChange(valueToApply); - } - setTempSelection(undefined); - } - }, - [ - variableData, - enableSelectAll, - effectiveAllAvailableOptionStrings, - tempSelection, - handleChange, - defaultValue, - ], - ); - - // Explicit function for callers to apply default on mount / data load - // Pass overrideOptions when freshly-loaded options aren't in state yet (async callers) - const applyDefaultIfNeeded = useCallback( - (overrideOptions?: (string | number | boolean)[]): void => { - if (isEmpty(selectValue)) { - const defaultValueFromOptions = getDefaultValue(overrideOptions); - if (defaultValueFromOptions !== undefined) { - handleChange(defaultValueFromOptions as string | string[]); - } - } - }, - [selectValue, handleChange, getDefaultValue], - ); - - const handleClear = useCallback((): void => { - handleChange([]); - }, [handleChange]); - - const value = variableData.multiSelect - ? tempSelection || selectValue - : selectValue; - - const onChange = useMemo(() => { - return variableData.multiSelect ? handleTempChange : handleSingleSelectChange; - }, [variableData.multiSelect, handleTempChange, handleSingleSelectChange]); - - return { - tempSelection, - setTempSelection, - enableSelectAll, - onDropdownVisibleChange, - handleClear, - value, - defaultValue, - onChange, - applyDefaultIfNeeded, - getDefaultValue, - }; -} diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/useDashboardVariableUpdate.ts b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/useDashboardVariableUpdate.ts deleted file mode 100644 index 96ecd75b5d5..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/useDashboardVariableUpdate.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* eslint-disable sonarjs/cognitive-complexity */ -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { toast } from '@signozhq/ui/sonner'; -import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels'; -import { updateLocalStorageDashboardVariable } from 'hooks/dashboard/useDashboardFromLocalStorage'; -import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; -import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes'; -import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; -import { v4 as uuidv4 } from 'uuid'; -import { useShallow } from 'zustand/react/shallow'; - -import { convertVariablesToDbFormat } from './util'; - -// Note: This logic completely mimics the logic in DashboardVariableSelection.tsx -// but is separated to avoid unnecessary logic addition. -interface UseDashboardVariableUpdateReturn { - onValueUpdate: ( - name: string, - id: string, - value: IDashboardVariable['selectedValue'], - allSelected: boolean, - haveCustomValuesSelected?: boolean, - ) => void; - createVariable: ( - name: string, - value: IDashboardVariable['selectedValue'], - // type?: IDashboardVariable['type'], - description?: string, - source?: 'logs' | 'traces' | 'metrics' | 'all sources', - widgetId?: string, - ) => void; - updateVariables: ( - updatedVariablesData: IDashboardVariables, - currentRequestedId?: string, - widgetIds?: string[], - applyToAll?: boolean, - ) => void; -} - -export const useDashboardVariableUpdate = - (): UseDashboardVariableUpdateReturn => { - const { dashboardId, dashboardData, setDashboardData } = useDashboardStore( - useShallow((s) => ({ - dashboardId: s.dashboardData?.id ?? '', - dashboardData: s.dashboardData, - setDashboardData: s.setDashboardData, - })), - ); - const addDynamicVariableToPanels = useAddDynamicVariableToPanels(); - const updateMutation = useUpdateDashboard(); - const { t } = useTranslation('dashboard'); - - const onValueUpdate = useCallback( - ( - name: string, - id: string, - value: IDashboardVariable['selectedValue'], - allSelected: boolean, - haveCustomValuesSelected?: boolean, - ): void => { - if (id) { - // Performance optimization: For dynamic variables with allSelected=true, we don't store - // individual values in localStorage since we can always derive them from available options. - // This makes localStorage much lighter and more efficient. - // currently all the variables are dynamic - const isDynamic = true; - updateLocalStorageDashboardVariable( - dashboardId, - name, - value, - allSelected, - isDynamic, - ); - - if (dashboardData) { - setDashboardData((prev) => { - if (prev) { - const oldVariables = prev?.data.variables; - // this is added to handle case where we have two different - // schemas for variable response - const updatedVariables = { ...oldVariables }; - if (updatedVariables?.[id]) { - updatedVariables[id] = { - ...updatedVariables[id], - selectedValue: value, - allSelected, - haveCustomValuesSelected, - }; - } - if (updatedVariables?.[name]) { - updatedVariables[name] = { - ...updatedVariables[name], - selectedValue: value, - allSelected, - haveCustomValuesSelected, - }; - } - return { - ...prev, - data: { - ...prev?.data, - variables: updatedVariables, - }, - }; - } - return prev; - }); - } - } - }, - [dashboardId, dashboardData, setDashboardData], - ); - - const updateVariables = useCallback( - ( - updatedVariablesData: IDashboardVariables, - currentRequestedId?: string, - widgetIds?: string[], - applyToAll?: boolean, - ): void => { - if (!dashboardData) { - return; - } - - const newDashboard = - (currentRequestedId && - addDynamicVariableToPanels( - dashboardData, - updatedVariablesData[currentRequestedId || ''], - widgetIds, - applyToAll, - )) || - dashboardData; - - updateMutation.mutateAsync( - { - id: dashboardData.id, - - data: { - ...newDashboard.data, - variables: updatedVariablesData, - }, - }, - { - onSuccess: (updatedDashboard) => { - if (updatedDashboard.data) { - setDashboardData(updatedDashboard.data); - // notifications.success({ - // message: t('variable_updated_successfully'), - // }); - } - }, - }, - ); - }, - [ - dashboardData, - addDynamicVariableToPanels, - updateMutation, - setDashboardData, - ], - ); - - const createVariable = useCallback( - ( - name: string, - value: IDashboardVariable['selectedValue'], - // type: IDashboardVariable['type'] = 'DYNAMIC', - description = '', - source: 'logs' | 'traces' | 'metrics' | 'all sources' = 'all sources', - // widgetId?: string, - ): void => { - if (!dashboardData) { - console.warn('No dashboard selected for variable creation'); - return; - } - - // Get current dashboard variables - const currentVariables = dashboardData.data.variables || {}; - - const nameExists = Object.values(currentVariables).some( - (v) => v.name === name, - ); - if (nameExists) { - toast.error(t('variable_name_already_exists', { name, ns: 'dashboard' })); - return; - } - - // Create tableRowData like Dashboard Settings does - const tableRowData = []; - const variableOrderArr = []; - for (const [key, value] of Object.entries(currentVariables)) { - const { order, id } = value; - - tableRowData.push({ - key, - name: key, - ...currentVariables[key], - id, - }); - - if (order) { - variableOrderArr.push(order); - } - } - - // Sort by order - tableRowData.sort((a, b) => a.order - b.order); - variableOrderArr.sort((a, b) => a - b); - - // Create new variable - const nextOrder = - variableOrderArr.length > 0 ? Math.max(...variableOrderArr) + 1 : 0; - const newVariable: IDashboardVariable = { - id: uuidv4(), - name, - type: 'DYNAMIC', - description, - order: nextOrder, - selectedValue: value, - allSelected: false, - haveCustomValuesSelected: false, - sort: 'ASC', - multiSelect: true, - showALLOption: true, - dynamicVariablesAttribute: name, - dynamicVariablesSource: source, - queryValue: '', - }; - - // Add to tableRowData - tableRowData.push({ - key: newVariable.id, - ...newVariable, - id: newVariable.id, - }); - - // Convert to dashboard format and update - const updatedVariables = convertVariablesToDbFormat(tableRowData); - updateVariables(updatedVariables, newVariable.id, [], false); - }, - [dashboardData, updateVariables], - ); - - return { - onValueUpdate, - createVariable, - updateVariables, - }; - }; - -export default useDashboardVariableUpdate; diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/util.ts b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/util.ts deleted file mode 100644 index dbe7e5d2134..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/util.ts +++ /dev/null @@ -1,484 +0,0 @@ -import { OptionData } from 'components/NewSelect/types'; -import { SOMETHING_WENT_WRONG } from 'constants/api'; -import { textContainsVariableReference } from 'lib/dashboardVariables/variableReference'; -import { isEmpty } from 'lodash-es'; -import { - IDashboardVariables, - IDependencyData, -} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes'; -import { - onVariableFetchComplete, - onVariableFetchFailure, - variableFetchStore, -} from 'providers/Dashboard/store/variableFetchStore'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -export function areArraysEqual( - a: (string | number | boolean)[], - b: (string | number | boolean)[], -): boolean { - if (a.length !== b.length) { - return false; - } - - for (let i = 0; i < a.length; i += 1) { - if (a[i] !== b[i]) { - return false; - } - } - - return true; -} - -export const convertVariablesToDbFormat = ( - variblesArr: IDashboardVariable[], -): IDashboardVariables => - variblesArr.reduce((result, obj: IDashboardVariable) => { - const { id } = obj; - - // @ts-expect-error - result[id] = obj; - return result; - }, {}); - -const getDependentVariablesBasedOnVariableName = ( - variableName: string, - variables: IDashboardVariable[], -): string[] => { - if (!variables || !Array.isArray(variables)) { - return []; - } - - return variables - .map((variable) => { - if (variable.type === 'QUERY') { - const queryValue = variable.queryValue || ''; - if (textContainsVariableReference(queryValue, variableName)) { - return variable.name; - } - } - return null; - }) - .filter((val): val is string => val !== null); -}; -export type VariableGraph = Record; - -export const buildDependencies = ( - variables: IDashboardVariable[], -): VariableGraph => { - const graph: VariableGraph = {}; - - // Initialize empty arrays for all variables first - variables.forEach((variable) => { - if (variable.name) { - graph[variable.name] = []; - } - }); - - // For each QUERY variable, add it as a dependent to its referenced variables - variables.forEach((variable) => { - if (variable.name) { - const dependentVariables = getDependentVariablesBasedOnVariableName( - variable.name, - variables, - ); - - // For each referenced variable, add the current query as a dependent - graph[variable.name] = dependentVariables; - } - }); - - return graph; -}; - -export const buildParentDependencyGraph = ( - graph: VariableGraph, -): VariableGraph => { - const parentGraph: VariableGraph = {}; - - // Initialize empty arrays for all nodes - Object.keys(graph).forEach((node) => { - parentGraph[node] = []; - }); - - // For each node and its children in the original graph - Object.entries(graph).forEach(([node, children]) => { - // For each child, add the current node as its parent - children.forEach((child) => { - if (!parentGraph[child]) { - parentGraph[child] = []; - } - parentGraph[child].push(node); - }); - }); - - return parentGraph; -}; - -const collectCyclePath = ( - graph: VariableGraph, - start: string, - end: string, -): string[] => { - const path: string[] = []; - let current = start; - - const findParent = (node: string): string | undefined => - Object.keys(graph).find((key) => graph[key]?.includes(node)); - - while (current !== end) { - const parent = findParent(current); - if (!parent) { - break; - } - path.push(parent); - current = parent; - } - - return [start, ...path]; -}; - -const detectCycle = ( - graph: VariableGraph, - node: string, - visited: Set, - recStack: Set, -): string[] | null => { - if (!visited.has(node)) { - visited.add(node); - recStack.add(node); - - const neighbors = graph[node] || []; - let cycleNodes: string[] | null = null; - - neighbors.some((neighbor) => { - if (!visited.has(neighbor)) { - const foundCycle = detectCycle(graph, neighbor, visited, recStack); - if (foundCycle) { - cycleNodes = foundCycle; - return true; - } - } else if (recStack.has(neighbor)) { - // Found a cycle, collect the cycle nodes - cycleNodes = collectCyclePath(graph, node, neighbor); - return true; - } - return false; - }); - - if (cycleNodes) { - return cycleNodes; - } - } - recStack.delete(node); - return null; -}; - -export const buildDependencyGraph = ( - dependencies: VariableGraph, - // eslint-disable-next-line sonarjs/cognitive-complexity -): IDependencyData => { - const inDegree: Record = {}; - const adjList: VariableGraph = {}; - - // Initialize in-degree and adjacency list - Object.keys(dependencies).forEach((node) => { - if (!inDegree[node]) { - inDegree[node] = 0; - } - if (!adjList[node]) { - adjList[node] = []; - } - dependencies[node]?.forEach((child) => { - if (!inDegree[child]) { - inDegree[child] = 0; - } - inDegree[child]++; - adjList[node].push(child); - }); - }); - - // Detect cycles - const visited = new Set(); - const recStack = new Set(); - let cycleNodes: string[] | undefined; - - Object.keys(dependencies).some((node) => { - if (!visited.has(node)) { - const foundCycle = detectCycle(dependencies, node, visited, recStack); - if (foundCycle) { - cycleNodes = foundCycle; - return true; - } - } - return false; - }); - - // Topological sort using Kahn's Algorithm - const queue: string[] = Object.keys(inDegree).filter( - (node) => inDegree[node] === 0, - ); - const topologicalOrder: string[] = []; - - while (queue.length > 0) { - const current = queue.shift(); - if (current === undefined) { - break; - } - topologicalOrder.push(current); - - adjList[current]?.forEach((neighbor) => { - inDegree[neighbor]--; - if (inDegree[neighbor] === 0) { - queue.push(neighbor); - } - }); - } - - const hasCycle = topologicalOrder.length !== Object.keys(dependencies)?.length; - - // Pre-compute transitive descendants by walking topological order in reverse. - // Each node's transitive descendants = direct children + their transitive descendants. - const transitiveDescendants: VariableGraph = {}; - for (let i = topologicalOrder.length - 1; i >= 0; i--) { - const node = topologicalOrder[i]; - const desc = new Set(); - for (const child of adjList[node] || []) { - desc.add(child); - for (const d of transitiveDescendants[child] || []) { - desc.add(d); - } - } - transitiveDescendants[node] = Array.from(desc); - } - - return { - order: topologicalOrder, - graph: adjList, - parentDependencyGraph: buildParentDependencyGraph(adjList), - transitiveDescendants, - hasCycle, - cycleNodes, - }; -}; - -export const onUpdateVariableNode = ( - nodeToUpdate: string, - graph: VariableGraph, - topologicalOrder: string[], - callback: (node: string) => void, -): void => { - const visited = new Set(); - - // If nodeToUpdate is not in topologicalOrder (e.g., CUSTOM variable), - // we still need to mark its children as needing updates - if (!topologicalOrder.includes(nodeToUpdate)) { - // Mark direct children of the node as visited so they get processed - (graph[nodeToUpdate] || []).forEach((child) => { - visited.add(child); - }); - } - - // Start processing from the node to update - topologicalOrder.forEach((node) => { - if (node === nodeToUpdate || visited.has(node)) { - visited.add(node); - callback(node); - (graph[node] || []).forEach((child) => { - visited.add(child); - }); - } - }); -}; - -export const getOptionsForDynamicVariable = ( - normalizedValues: (string | number | boolean)[], - relatedValues: string[], -): OptionData[] => { - const options: OptionData[] = []; - - if (relatedValues.length > 0) { - // Add Related Values group - options.push({ - label: 'Related Values', - value: 'relatedValues', - options: relatedValues.map((option) => ({ - label: option.toString(), - value: option.toString(), - })), - }); - - // Add All Values group (complete union - shows everything) - options.push({ - label: 'All Values', - value: 'allValues', - options: normalizedValues.map((option) => ({ - label: option.toString(), - value: option.toString(), - })), - }); - - return options; - } - - return normalizedValues.map((option) => ({ - label: option.toString(), - value: option.toString(), - })); -}; - -export const uniqueOptions = (options: OptionData[]): OptionData[] => { - const uniqueOptions: OptionData[] = []; - const seenValues = new Set(); - - options.forEach((option) => { - const value = option.value || ''; - if (seenValues.has(value)) { - return; - } - seenValues.add(value); - uniqueOptions.push(option); - }); - - return uniqueOptions; -}; - -export const getSelectValue = ( - selectedValue: IDashboardVariable['selectedValue'], - variableData: IDashboardVariable, -): string | string[] | undefined => { - if (Array.isArray(selectedValue)) { - if (!variableData.multiSelect && selectedValue.length === 1) { - return selectedValue[0]?.toString(); - } - return selectedValue.map((item) => item.toString()); - } - return selectedValue?.toString(); -}; - -/** - * Merges multiple arrays of values into a single deduplicated string array. - */ -export function mergeUniqueStrings( - ...arrays: (string | number | boolean)[][] -): string[] { - return [...new Set(arrays.flatMap((arr) => arr.map((v) => v.toString())))]; -} - -function isEligibleFilterVariable( - variable: IDashboardVariable, - currentVariableId: string, -): boolean { - if (variable.id === currentVariableId) { - return false; - } - if (variable.type !== 'DYNAMIC') { - return false; - } - if (!variable.dynamicVariablesAttribute) { - return false; - } - if (!variable.selectedValue || isEmpty(variable.selectedValue)) { - return false; - } - return !(variable.showALLOption && variable.allSelected); -} - -function formatQueryValue(val: string): string { - const numValue = Number(val); - if (!Number.isNaN(numValue) && Number.isFinite(numValue)) { - return val; - } - return `'${val.replace(/'/g, "\\'")}'`; -} - -function buildQueryPart(attribute: string, values: string[]): string { - const formatted = values.map(formatQueryValue); - if (formatted.length === 1) { - return `${attribute} = ${formatted[0]}`; - } - return `${attribute} IN [${formatted.join(', ')}]`; -} - -/** - * Builds a filter query string from sibling dynamic variables' selected values. - * e.g. `k8s.namespace.name IN ['zeus', 'gene'] AND doc_op_type = 'test'` - */ -export function buildExistingDynamicVariableQuery( - existingVariables: IDashboardVariables | null, - currentVariableId: string, - hasDynamicAttribute: boolean, -): string { - if (!existingVariables || !hasDynamicAttribute) { - return ''; - } - - const queryParts: string[] = []; - - for (const variable of Object.values(existingVariables)) { - // Skip the current variable being processed - if (!isEligibleFilterVariable(variable, currentVariableId)) { - continue; - } - - const rawValues = Array.isArray(variable.selectedValue) - ? variable.selectedValue - : [variable.selectedValue]; - - // Filter out empty values and convert to strings - const validValues = rawValues - .filter( - (val): val is string | number | boolean => - val !== null && val !== undefined && val !== '', - ) - .map((val) => val.toString()); - - if (validValues.length > 0 && variable.dynamicVariablesAttribute) { - queryParts.push( - buildQueryPart(variable.dynamicVariablesAttribute, validValues), - ); - } - } - - return queryParts.join(' AND '); -} - -function isVariableInActiveFetchState(state: string | undefined): boolean { - return state === 'loading' || state === 'revalidating'; -} - -/** - * Completes or fails a variable's fetch state machine transition. - * No-ops if the variable is not currently in an active fetch state. - */ -export function settleVariableFetch( - name: string | undefined, - outcome: 'complete' | 'failure', -): void { - if (!name) { - return; - } - - const currentState = variableFetchStore.getSnapshot().states[name]; - if (!isVariableInActiveFetchState(currentState)) { - return; - } - - if (outcome === 'complete') { - onVariableFetchComplete(name); - } else { - onVariableFetchFailure(name); - } -} - -export function extractErrorMessage( - error: { message?: string } | null, -): string { - if (!error) { - return SOMETHING_WENT_WRONG; - } - return ( - error.message || - 'Please make sure configuration is valid and you have required setup and permissions' - ); -} diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/utils.test.ts b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/utils.test.ts deleted file mode 100644 index b2158f273d1..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/utils.test.ts +++ /dev/null @@ -1,531 +0,0 @@ -jest.mock('providers/Dashboard/store/variableFetchStore', () => ({ - variableFetchStore: { - getSnapshot: jest.fn(), - }, - onVariableFetchComplete: jest.fn(), - onVariableFetchFailure: jest.fn(), -})); - -import { - onVariableFetchComplete, - onVariableFetchFailure, - variableFetchStore, -} from 'providers/Dashboard/store/variableFetchStore'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -import { - areArraysEqual, - buildExistingDynamicVariableQuery, - extractErrorMessage, - mergeUniqueStrings, - onUpdateVariableNode, - settleVariableFetch, - VariableGraph, -} from './util'; - -// ──────────────────────────────────────────────────────────────── -// Existing tests -// ──────────────────────────────────────────────────────────────── - -describe('areArraysEqual', () => { - it('should return true for equal arrays with same order', () => { - const array1 = [1, 'a', true, 5, 'hello']; - const array2 = [1, 'a', true, 5, 'hello']; - expect(areArraysEqual(array1, array2)).toBe(true); - }); - - it('should return false for equal arrays with different order', () => { - const array1 = [1, 'a', true, 5, 'hello']; - const array2 = ['hello', 1, true, 'a', 5]; - expect(areArraysEqual(array1, array2)).toBe(false); - }); - - it('should return false for arrays with different lengths', () => { - const array1 = [1, 'a', true, 5, 'hello']; - const array2 = [1, 'a', true, 5]; - expect(areArraysEqual(array1, array2)).toBe(false); - }); - - it('should return false for arrays with different elements', () => { - const array1 = [1, 'a', true, 5, 'hello']; - const array2 = [1, 'a', true, 5, 'world']; - expect(areArraysEqual(array1, array2)).toBe(false); - }); - - it('should return true for empty arrays', () => { - const array1: string[] = []; - const array2: string[] = []; - expect(areArraysEqual(array1, array2)).toBe(true); - }); -}); - -describe('onUpdateVariableNode', () => { - // Graph structure: - // deployment -> namespace -> service -> pod - // deployment has no parents, namespace depends on deployment, etc. - const graph: VariableGraph = { - deployment: ['namespace'], - namespace: ['service'], - service: ['pod'], - pod: [], - customVar: ['namespace'], // CUSTOM variable that affects namespace - }; - - const topologicalOrder = ['deployment', 'namespace', 'service', 'pod']; - - it('should call callback for the node and all its descendants', () => { - const visited: string[] = []; - const callback = (node: string): void => { - visited.push(node); - }; - - onUpdateVariableNode('deployment', graph, topologicalOrder, callback); - - expect(visited).toStrictEqual(['deployment', 'namespace', 'service', 'pod']); - }); - - it('should call callback starting from a middle node', () => { - const visited: string[] = []; - const callback = (node: string): void => { - visited.push(node); - }; - - onUpdateVariableNode('namespace', graph, topologicalOrder, callback); - - expect(visited).toStrictEqual(['namespace', 'service', 'pod']); - }); - - it('should only call callback for the leaf node when updating leaf', () => { - const visited: string[] = []; - const callback = (node: string): void => { - visited.push(node); - }; - - onUpdateVariableNode('pod', graph, topologicalOrder, callback); - - expect(visited).toStrictEqual(['pod']); - }); - - it('should handle CUSTOM variable not in topologicalOrder by updating its children', () => { - const visited: string[] = []; - const callback = (node: string): void => { - visited.push(node); - }; - - // customVar is not in topologicalOrder but has namespace as a child - onUpdateVariableNode('customVar', graph, topologicalOrder, callback); - - // Should process namespace and its descendants (service, pod) - expect(visited).toStrictEqual(['namespace', 'service', 'pod']); - }); - - it('should handle node not in graph gracefully', () => { - const visited: string[] = []; - const callback = (node: string): void => { - visited.push(node); - }; - - onUpdateVariableNode('unknownNode', graph, topologicalOrder, callback); - - // Should not call callback for any node since unknownNode has no children - expect(visited).toStrictEqual([]); - }); - - it('should handle empty graph', () => { - const visited: string[] = []; - const callback = (node: string): void => { - visited.push(node); - }; - - onUpdateVariableNode('deployment', {}, topologicalOrder, callback); - - // deployment is in topologicalOrder, so callback is called for it - expect(visited).toStrictEqual(['deployment']); - }); - - it('should handle empty topologicalOrder', () => { - const visited: string[] = []; - const callback = (node: string): void => { - visited.push(node); - }; - - onUpdateVariableNode('deployment', graph, [], callback); - - expect(visited).toStrictEqual([]); - }); - - it('should handle CUSTOM variable with multiple children', () => { - const graphWithMultipleChildren: VariableGraph = { - ...graph, - customMulti: ['namespace', 'service'], // CUSTOM variable affecting multiple nodes - }; - - const visited: string[] = []; - const callback = (node: string): void => { - visited.push(node); - }; - - onUpdateVariableNode( - 'customMulti', - graphWithMultipleChildren, - topologicalOrder, - callback, - ); - - // Should process namespace, service, and pod (descendants) - expect(visited).toStrictEqual(['namespace', 'service', 'pod']); - }); -}); - -// ──────────────────────────────────────────────────────────────── -// New tests for functions added in recent commits -// ──────────────────────────────────────────────────────────────── - -function makeDynamicVar( - overrides: Partial & { id: string }, -): IDashboardVariable { - return { - name: overrides.id, - description: '', - type: 'DYNAMIC', - sort: 'DISABLED', - multiSelect: false, - showALLOption: false, - allSelected: false, - dynamicVariablesAttribute: 'attr', - selectedValue: 'some-value', - ...overrides, - } as IDashboardVariable; -} - -describe('mergeUniqueStrings', () => { - it('should merge two arrays and deduplicate', () => { - expect(mergeUniqueStrings(['a', 'b'], ['b', 'c'])).toStrictEqual([ - 'a', - 'b', - 'c', - ]); - }); - - it('should convert numbers and booleans to strings', () => { - expect(mergeUniqueStrings([1, true, 'hello'], [2, false])).toStrictEqual([ - '1', - 'true', - 'hello', - '2', - 'false', - ]); - }); - - it('should deduplicate when number and its string form both appear', () => { - expect(mergeUniqueStrings([42], ['42'])).toStrictEqual(['42']); - }); - - it('should handle a single array', () => { - expect(mergeUniqueStrings(['x', 'y', 'x'])).toStrictEqual(['x', 'y']); - }); - - it('should handle three or more arrays', () => { - expect(mergeUniqueStrings(['a'], ['b'], ['c'], ['a', 'c'])).toStrictEqual([ - 'a', - 'b', - 'c', - ]); - }); - - it('should return empty array when no arrays are provided', () => { - expect(mergeUniqueStrings()).toStrictEqual([]); - }); - - it('should return empty array when all input arrays are empty', () => { - expect(mergeUniqueStrings([], [], [])).toStrictEqual([]); - }); - - it('should preserve order of first occurrence', () => { - expect(mergeUniqueStrings(['c', 'a'], ['b', 'a'])).toStrictEqual([ - 'c', - 'a', - 'b', - ]); - }); -}); - -describe('buildExistingDynamicVariableQuery', () => { - // --- Guard clauses --- - it('should return empty string when existingVariables is null', () => { - expect(buildExistingDynamicVariableQuery(null, 'v1', true)).toBe(''); - }); - - it('should return empty string when hasDynamicAttribute is false', () => { - const variables = { v2: makeDynamicVar({ id: 'v2' }) }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', false)).toBe(''); - }); - - // --- Eligibility filtering --- - it('should skip the current variable (same id)', () => { - const variables = { - v1: makeDynamicVar({ - id: 'v1', - dynamicVariablesAttribute: 'ns', - selectedValue: 'prod', - }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe(''); - }); - - it('should skip non-DYNAMIC variables', () => { - const variables = { - v2: makeDynamicVar({ id: 'v2', type: 'QUERY' as any }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe(''); - }); - - it('should skip variables without dynamicVariablesAttribute', () => { - const variables = { - v2: makeDynamicVar({ - id: 'v2', - dynamicVariablesAttribute: undefined, - selectedValue: 'val', - }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe(''); - }); - - it('should skip variables with null selectedValue', () => { - const variables = { - v2: makeDynamicVar({ id: 'v2', selectedValue: null }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe(''); - }); - - it('should skip variables with empty string selectedValue', () => { - const variables = { - v2: makeDynamicVar({ id: 'v2', selectedValue: '' }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe(''); - }); - - it('should skip variables with empty array selectedValue', () => { - const variables = { - v2: makeDynamicVar({ id: 'v2', selectedValue: [] }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe(''); - }); - - it('should skip variables where showALLOption and allSelected are both true', () => { - const variables = { - v2: makeDynamicVar({ - id: 'v2', - showALLOption: true, - allSelected: true, - dynamicVariablesAttribute: 'ns', - selectedValue: 'prod', - }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe(''); - }); - - it('should include variable with showALLOption true but allSelected false', () => { - const variables = { - v2: makeDynamicVar({ - id: 'v2', - showALLOption: true, - allSelected: false, - dynamicVariablesAttribute: 'ns', - selectedValue: 'prod', - }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe( - "ns = 'prod'", - ); - }); - - // --- Value formatting --- - it('should quote string values in the query', () => { - const variables = { - v2: makeDynamicVar({ - id: 'v2', - dynamicVariablesAttribute: 'k8s.namespace.name', - selectedValue: 'zeus', - }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe( - "k8s.namespace.name = 'zeus'", - ); - }); - - it('should leave numeric values unquoted', () => { - const variables = { - v2: makeDynamicVar({ - id: 'v2', - dynamicVariablesAttribute: 'http.status_code', - selectedValue: '200', - }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe( - 'http.status_code = 200', - ); - }); - - it('should escape single quotes in string values', () => { - const variables = { - v2: makeDynamicVar({ - id: 'v2', - dynamicVariablesAttribute: 'user.name', - selectedValue: "O'Brien", - }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe( - "user.name = 'O\\'Brien'", - ); - }); - - it('should build an IN clause for array selectedValue with multiple items', () => { - const variables = { - v2: makeDynamicVar({ - id: 'v2', - dynamicVariablesAttribute: 'k8s.namespace.name', - selectedValue: ['zeus', 'gene'], - }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe( - "k8s.namespace.name IN ['zeus', 'gene']", - ); - }); - - it('should handle mix of numeric and string values in IN clause', () => { - const variables = { - v2: makeDynamicVar({ - id: 'v2', - dynamicVariablesAttribute: 'http.status_code', - selectedValue: ['200', 'unknown'], - }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe( - "http.status_code IN [200, 'unknown']", - ); - }); - - it('should filter out empty string values from array', () => { - const variables = { - v2: makeDynamicVar({ - id: 'v2', - dynamicVariablesAttribute: 'region', - selectedValue: ['us-east', '', 'eu-west'], - }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe( - "region IN ['us-east', 'eu-west']", - ); - }); - - // --- Multiple siblings --- - it('should join multiple sibling variables with AND', () => { - const variables = { - v2: makeDynamicVar({ - id: 'v2', - dynamicVariablesAttribute: 'k8s.namespace.name', - selectedValue: ['zeus', 'gene'], - }), - v3: makeDynamicVar({ - id: 'v3', - dynamicVariablesAttribute: 'doc_op_type', - selectedValue: 'test', - }), - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe( - "k8s.namespace.name IN ['zeus', 'gene'] AND doc_op_type = 'test'", - ); - }); - - it('should return empty string when no variables are eligible', () => { - const variables = { - v1: makeDynamicVar({ id: 'v1' }), // same as current — skipped - v2: makeDynamicVar({ id: 'v2', type: 'QUERY' as any }), // not DYNAMIC - v3: makeDynamicVar({ id: 'v3', selectedValue: null }), // no value - }; - expect(buildExistingDynamicVariableQuery(variables, 'v1', true)).toBe(''); - }); -}); - -describe('settleVariableFetch', () => { - const mockGetSnapshot = variableFetchStore.getSnapshot as jest.Mock; - const mockComplete = onVariableFetchComplete as jest.Mock; - const mockFailure = onVariableFetchFailure as jest.Mock; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should no-op when name is undefined', () => { - settleVariableFetch(undefined, 'complete'); - expect(mockGetSnapshot).not.toHaveBeenCalled(); - expect(mockComplete).not.toHaveBeenCalled(); - expect(mockFailure).not.toHaveBeenCalled(); - }); - - it.each(['idle', 'waiting', 'error', undefined] as const)( - 'should no-op when variable state is %s', - (state) => { - mockGetSnapshot.mockReturnValue({ states: { myVar: state } }); - settleVariableFetch('myVar', 'complete'); - expect(mockComplete).not.toHaveBeenCalled(); - expect(mockFailure).not.toHaveBeenCalled(); - }, - ); - - it('should call onVariableFetchComplete when state is loading and outcome is complete', () => { - mockGetSnapshot.mockReturnValue({ states: { myVar: 'loading' } }); - settleVariableFetch('myVar', 'complete'); - expect(mockComplete).toHaveBeenCalledWith('myVar'); - expect(mockFailure).not.toHaveBeenCalled(); - }); - - it('should call onVariableFetchComplete when state is revalidating and outcome is complete', () => { - mockGetSnapshot.mockReturnValue({ states: { myVar: 'revalidating' } }); - settleVariableFetch('myVar', 'complete'); - expect(mockComplete).toHaveBeenCalledWith('myVar'); - expect(mockFailure).not.toHaveBeenCalled(); - }); - - it('should call onVariableFetchFailure when state is loading and outcome is failure', () => { - mockGetSnapshot.mockReturnValue({ states: { myVar: 'loading' } }); - settleVariableFetch('myVar', 'failure'); - expect(mockFailure).toHaveBeenCalledWith('myVar'); - expect(mockComplete).not.toHaveBeenCalled(); - }); - - it('should call onVariableFetchFailure when state is revalidating and outcome is failure', () => { - mockGetSnapshot.mockReturnValue({ states: { myVar: 'revalidating' } }); - settleVariableFetch('myVar', 'failure'); - expect(mockFailure).toHaveBeenCalledWith('myVar'); - expect(mockComplete).not.toHaveBeenCalled(); - }); -}); - -describe('extractErrorMessage', () => { - const FALLBACK_MESSAGE = - 'Please make sure configuration is valid and you have required setup and permissions'; - - it('should return SOMETHING_WENT_WRONG when error is null', () => { - expect(extractErrorMessage(null)).toBe('Something went wrong'); - }); - - it('should return the error message when it exists', () => { - expect(extractErrorMessage({ message: 'Query timeout' })).toBe( - 'Query timeout', - ); - }); - - it('should return fallback when error object has no message property', () => { - expect(extractErrorMessage({})).toBe(FALLBACK_MESSAGE); - }); - - it('should return fallback when error.message is empty string', () => { - expect(extractErrorMessage({ message: '' })).toBe(FALLBACK_MESSAGE); - }); - - it('should return fallback when error.message is undefined', () => { - expect(extractErrorMessage({ message: undefined })).toBe(FALLBACK_MESSAGE); - }); -}); diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/customVariableSelectStrategy.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/customVariableSelectStrategy.tsx deleted file mode 100644 index 1fbe0fe1f55..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/customVariableSelectStrategy.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { defaultVariableSelectStrategy } from './defaultVariableSelectStrategy'; -import { VariableSelectStrategy } from './variableSelectStrategyTypes'; - -export const customVariableSelectStrategy: VariableSelectStrategy = - defaultVariableSelectStrategy; diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/defaultVariableSelectStrategy.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/defaultVariableSelectStrategy.tsx deleted file mode 100644 index 3a839b8f033..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/defaultVariableSelectStrategy.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { VariableSelectStrategy } from './variableSelectStrategyTypes'; - -export const defaultVariableSelectStrategy: VariableSelectStrategy = { - handleChange({ value, variableData, optionsData, onValueUpdate }) { - if (!variableData.name) { - return; - } - - const isAllSelected = - Array.isArray(value) && - value.length > 0 && - optionsData.every((option) => value.includes(option.toString())); - - if (isAllSelected && variableData.showALLOption) { - onValueUpdate(variableData.name, variableData.id, optionsData, true); - } else { - onValueUpdate(variableData.name, variableData.id, value, false); - } - }, -}; diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/dynamicVariableSelectStrategy.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/dynamicVariableSelectStrategy.tsx deleted file mode 100644 index 97ec71ad43b..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/dynamicVariableSelectStrategy.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { ALL_SELECT_VALUE } from 'container/DashboardContainer/utils'; - -import { VariableSelectStrategy } from './variableSelectStrategyTypes'; - -export const dynamicVariableSelectStrategy: VariableSelectStrategy = { - handleChange({ - value, - variableData, - allAvailableOptionStrings, - onValueUpdate, - }) { - if (!variableData.name) { - return; - } - - if ( - value === ALL_SELECT_VALUE || - (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) - ) { - onValueUpdate(variableData.name, variableData.id, null, true); - } else { - // For ALL selection in dynamic variables, pass null to avoid storing values - // The parent component will handle this appropriately - const haveCustomValuesSelected = - Array.isArray(value) && - !value.every((v) => allAvailableOptionStrings.includes(v.toString())); - - onValueUpdate( - variableData.name, - variableData.id, - value, - allAvailableOptionStrings.every((v) => value.includes(v.toString())), - haveCustomValuesSelected, - ); - } - }, -}; diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/queryVariableSelectStrategy.tsx b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/queryVariableSelectStrategy.tsx deleted file mode 100644 index 4e3eba8b88a..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/queryVariableSelectStrategy.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { defaultVariableSelectStrategy } from './defaultVariableSelectStrategy'; -import { VariableSelectStrategy } from './variableSelectStrategyTypes'; - -export const queryVariableSelectStrategy: VariableSelectStrategy = - defaultVariableSelectStrategy; diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/variableSelectStrategyTypes.ts b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/variableSelectStrategyTypes.ts deleted file mode 100644 index f14917bbe23..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/variableSelectStrategyTypes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { VariableItemProps } from '../VariableItem'; - -export interface VariableSelectStrategy { - handleChange(params: { - value: string | string[]; - variableData: VariableItemProps['variableData']; - onValueUpdate: VariableItemProps['onValueUpdate']; - optionsData: (string | number | boolean)[]; - allAvailableOptionStrings: string[]; - }): void; -} diff --git a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/variableSelectStrategyUtils.ts b/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/variableSelectStrategyUtils.ts deleted file mode 100644 index ae57754e726..00000000000 --- a/frontend/src/container/DashboardContainer/DashboardVariablesSelection/variableSelectStrategy/variableSelectStrategyUtils.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { isEqual } from 'lodash-es'; - -export const areArraysEqualIgnoreOrder = ( - a: (string | number | boolean)[], - b: (string | number | boolean)[], -): boolean => { - if (a.length !== b.length) { - return false; - } - const sortedA = [...a].sort(); - const sortedB = [...b].sort(); - return isEqual(sortedA, sortedB); -}; - -export const uniqueValues = (values: string[] | string): string[] | string => { - if (Array.isArray(values)) { - const uniqueValues: string[] = []; - const seenValues = new Set(); - - values.forEach((value) => { - if (seenValues.has(value)) { - return; - } - seenValues.add(value); - uniqueValues.push(value); - }); - - return uniqueValues; - } - - return values; -}; diff --git a/frontend/src/container/DashboardContainer/GridGraphs/index.tsx b/frontend/src/container/DashboardContainer/GridGraphs/index.tsx deleted file mode 100644 index 3da5f0656ae..00000000000 --- a/frontend/src/container/DashboardContainer/GridGraphs/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { FullScreenHandle } from 'react-full-screen'; -import GridGraphLayout from 'container/GridCardLayout'; -import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils'; - -import { GridComponentSliderContainer } from './styles'; - -interface GridGraphsProps { - handle: FullScreenHandle; -} - -function GridGraphs(props: GridGraphsProps): JSX.Element { - const { handle } = props; - return ( - - - - ); -} - -export default GridGraphs; diff --git a/frontend/src/container/DashboardContainer/GridGraphs/styles.ts b/frontend/src/container/DashboardContainer/GridGraphs/styles.ts deleted file mode 100644 index 1bc68c27b03..00000000000 --- a/frontend/src/container/DashboardContainer/GridGraphs/styles.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Card as CardComponent, Row } from 'antd'; -import styled from 'styled-components'; - -export const CardContainer = styled(Row)` - &&& { - margin-top: 1rem; - } -`; - -export const Card = styled(CardComponent)` - &&& { - cursor: pointer; - } - .ant-card-body { - display: flex; - width: 100%; - justify-content: center; - align-items: center; - - > span { - margin-right: 0.5rem; - } - } -`; - -export const GridComponentSliderContainer = styled.div` - margin-top: 1rem; -`; diff --git a/frontend/src/container/DashboardContainer/PanelTypeSelectionModal/PanelTypeSelectionModal.styles.scss b/frontend/src/container/DashboardContainer/PanelTypeSelectionModal/PanelTypeSelectionModal.styles.scss deleted file mode 100644 index 32a52157761..00000000000 --- a/frontend/src/container/DashboardContainer/PanelTypeSelectionModal/PanelTypeSelectionModal.styles.scss +++ /dev/null @@ -1,118 +0,0 @@ -.panel-type-selection-modal { - .ant-modal-content { - width: 515px; - max-height: 646px; - overflow-y: auto; - flex-shrink: 0; - border-radius: 4px; - border: 1px solid var(--l1-border); - background: var(--l2-background); - box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); - padding: 0px; - - .ant-modal-header { - height: 52px; - padding: 16px; - background: var(--l2-background); - border-bottom: 1px solid var(--l1-border); - margin-bottom: 0px; - - .ant-modal-title { - color: var(--l1-foreground); - 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; - gap: 16px; - - .selected { - background: var(--l1-border); - } - - .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(--l1-border); - - .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(--l2-foreground); - 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; - } - } - - .panel-type-text { - text-align: center; - margin-top: 1rem; - } - } - } - - .ant-modal-footer { - border-radius: 0px 0px 4px 4px; - border-top: 1px solid var(--l1-border); - background: var(--l2-background); - padding: 12px 15px; - margin-top: 0px; - - .ant-btn { - width: 100%; - display: flex; - align-items: center; - flex-direction: row-reverse; - justify-content: center; - color: var(--l1-foreground); - - /* button/ small */ - font-family: Inter; - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 24px; /* 200% */ - border-radius: 2px; - background: var(--primary-background); - padding: 4px 8px; - gap: 4px; - } - } - - &::-webkit-scrollbar { - width: 0.1rem; - } - } -} diff --git a/frontend/src/container/DashboardContainer/PanelTypeSelectionModal/index.tsx b/frontend/src/container/DashboardContainer/PanelTypeSelectionModal/index.tsx deleted file mode 100644 index 3019a3b9eb9..00000000000 --- a/frontend/src/container/DashboardContainer/PanelTypeSelectionModal/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { memo } from 'react'; -import { Card, Modal } from 'antd'; -import { Typography } from '@signozhq/ui/typography'; -import logEvent from 'api/common/logEvent'; -import { QueryParams } from 'constants/query'; -import { PANEL_TYPES, PANEL_TYPES_INITIAL_QUERY } from 'constants/queryBuilder'; -import createQueryParams from 'lib/createQueryParams'; -import history from 'lib/history'; -import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper'; -import { v4 as uuid } from 'uuid'; - -import { PanelTypesWithData } from './menuItems'; - -import './PanelTypeSelectionModal.styles.scss'; - -function PanelTypeSelectionModal(): JSX.Element { - const { isPanelTypeSelectionModalOpen, setIsPanelTypeSelectionModalOpen } = - usePanelTypeSelectionModalStore(); - - const onClickHandler = (name: PANEL_TYPES) => (): void => { - const id = uuid(); - setIsPanelTypeSelectionModalOpen(false); - logEvent('Dashboard Detail: New panel type selected', { - panelType: name, - widgetId: id, - }); - - const queryParams = { - graphType: name, - widgetId: id, - [QueryParams.compositeQuery]: JSON.stringify( - PANEL_TYPES_INITIAL_QUERY[name], - ), - }; - - history.push( - `${history.location.pathname}/new?${createQueryParams(queryParams)}`, - ); - }; - - const handleCardClick = (panelType: PANEL_TYPES): void => { - onClickHandler(panelType)(); - }; - - return ( - { - setIsPanelTypeSelectionModalOpen(false); - }} - rootClassName="panel-type-selection-modal" - footer={null} - title="New Panel" - > -
- {PanelTypesWithData.map(({ name, icon, display }) => ( - handleCardClick(name)} - id={name} - key={name} - data-testid={`panel-type-${name}`} - > - {icon} - {display} - - ))} -
-
- ); -} - -export default memo(PanelTypeSelectionModal); diff --git a/frontend/src/container/DashboardContainer/__test__/DynamicVariableDefaultBehavior.test.tsx b/frontend/src/container/DashboardContainer/__test__/DynamicVariableDefaultBehavior.test.tsx deleted file mode 100644 index e990fadd5d0..00000000000 --- a/frontend/src/container/DashboardContainer/__test__/DynamicVariableDefaultBehavior.test.tsx +++ /dev/null @@ -1,525 +0,0 @@ -import React from 'react'; -import { QueryClient, QueryClientProvider, useQuery } from 'react-query'; -// eslint-disable-next-line no-restricted-imports -import * as ReactRedux from 'react-redux'; -import { - act, - fireEvent, - render, - RenderResult, - screen, - waitFor, -} from '@testing-library/react'; -import { getFieldValues } from 'api/dynamicVariables/getFieldValues'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -import DynamicVariableInput from '../DashboardVariablesSelection/DynamicVariableInput'; - -// Mock useVariableFetchState to return "fetching" state so useQuery is enabled -jest.mock('hooks/dashboard/useVariableFetchState', () => ({ - useVariableFetchState: (): Record => ({ - variableFetchCycleId: 0, - variableFetchState: 'loading', - isVariableSettled: false, - isVariableFetching: true, - hasVariableFetchedOnce: false, - isVariableWaitingForDependencies: false, - variableDependencyWaitMessage: '', - }), -})); - -// Mock the getFieldValues API -jest.mock('api/dynamicVariables/getFieldValues', () => ({ - getFieldValues: jest.fn(), -})); - -// Mock useQuery from react-query -jest.mock('react-query', () => { - const originalModule = jest.requireActual('react-query'); - return { - ...originalModule, - useQuery: jest.fn(), - }; -}); - -describe('Dynamic Variable Default Behavior', () => { - const mockOnValueUpdate = jest.fn(); - const mockApiResponse = { - httpStatusCode: 200, - data: { - values: { - stringValues: ['frontend', 'backend', 'database', 'cache'], - }, - normalizedValues: ['frontend', 'backend', 'database', 'cache'], - complete: true, - }, - }; - - let queryClient: QueryClient; - - const renderWithQueryClient = (component: React.ReactElement): RenderResult => - render( - {component}, - ); - - beforeEach(() => { - // Mock scrollIntoView for JSDOM environment - window.HTMLElement.prototype.scrollIntoView = jest.fn(); - jest.clearAllMocks(); - - // Create a new QueryClient for each test - queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }); - - // Mock getFieldValues API to return our test data - (getFieldValues as jest.Mock).mockResolvedValue(mockApiResponse); - - // Mock useQuery implementation to avoid infinite re-renders - // and ensure onSuccess is called once - (useQuery as jest.Mock).mockImplementation((key, options) => { - const { onSuccess, enabled, queryFn } = options || {}; - const variableName = key[1]; - const dynamicVarsKey = key[2]; - - React.useEffect(() => { - if (enabled !== false) { - if (onSuccess) { - // For 'services' tests: - // 1. "Default to ALL" expectations imply empty options -> [] behavior. This happens when selectedValue is undefined (dynamicVarsKey has 'null'). - // 2. "ALL Option Special Value" needs full options to render the "ALL" item in dropdown. This happens when selectedValue is defined. - if ( - variableName === 'services' && - typeof dynamicVarsKey === 'string' && - dynamicVarsKey.includes('null') - ) { - onSuccess({ - ...mockApiResponse, - data: { ...mockApiResponse.data, normalizedValues: [] }, - }); - } else { - onSuccess(mockApiResponse); - } - } - if (queryFn) { - queryFn({ signal: undefined }); - } - } - }, [enabled, variableName, dynamicVarsKey]); // Only depend on enabled/keys - - return { - isLoading: false, - isError: false, - data: mockApiResponse, - refetch: jest.fn(), - }; - }); - - jest.spyOn(ReactRedux, 'useSelector').mockReturnValue({ - minTime: '2023-01-01T00:00:00Z', - maxTime: '2023-01-02T00:00:00Z', - }); - }); - - describe('Single Select Default Values', () => { - it('should use default value when no previous selection exists', async () => { - const variableData: IDashboardVariable = { - id: 'var1', - name: 'service', - type: 'DYNAMIC', - multiSelect: false, - defaultValue: 'backend', - selectedValue: undefined, - dynamicVariablesAttribute: 'service.name', - dynamicVariablesSource: 'Traces', - description: '', - sort: 'DISABLED', - showALLOption: false, - allSelected: false, - }; - - await act(async () => { - renderWithQueryClient( - , - ); - }); - - // Should call onValueUpdate with default value - expect(mockOnValueUpdate).toHaveBeenCalledWith( - 'service', - 'var1', - 'backend', - true, - false, - ); - }); - - it('should preserve previous selection over default value', async () => { - const variableData: IDashboardVariable = { - id: 'var1', - name: 'service', - type: 'DYNAMIC', - multiSelect: false, - defaultValue: 'backend', - selectedValue: 'frontend', - dynamicVariablesAttribute: 'service.name', - dynamicVariablesSource: 'Traces', - description: '', - sort: 'DISABLED', - showALLOption: false, - allSelected: false, - }; - - await act(async () => { - renderWithQueryClient( - , - ); - }); - - // Should NOT call onValueUpdate since previous selection exists - expect(mockOnValueUpdate).not.toHaveBeenCalledWith(); - - // Check if the previous selection 'frontend' is displayed in the UI - // For single select, the value should be visible in the select component - const selectElement = screen.getByRole('combobox'); - expect(selectElement).toBeInTheDocument(); - - // Open dropdown to check if 'frontend' is selected - await act(async () => { - fireEvent.mouseDown(selectElement); - }); - - // Check if 'frontend' option is present and selected in dropdown - const frontendOption = screen.getByRole('option', { name: 'frontend' }); - expect(frontendOption).toHaveClass('selected'); - }); - - it('should use first value when no default and no previous selection', async () => { - const variableData: IDashboardVariable = { - id: 'var1', - name: 'service', - type: 'DYNAMIC', - multiSelect: false, - defaultValue: undefined, - selectedValue: undefined, - dynamicVariablesAttribute: 'service.name', - dynamicVariablesSource: 'Traces', - description: '', - sort: 'DISABLED', - showALLOption: false, - allSelected: false, - }; - - await act(async () => { - renderWithQueryClient( - , - ); - }); - - // Check if the dropdown is present - const selectElement = screen.getByRole('combobox'); - expect(selectElement).toBeInTheDocument(); - - // Wait for API call to complete and data to be loaded - await waitFor(() => { - expect(getFieldValues).toHaveBeenCalledWith( - 'traces', - 'service.name', - '', - '2023-01-01T00:00:00Z', - '2023-01-02T00:00:00Z', - '', - undefined, // signal - ); - }); - - // Open dropdown to check available options - await act(async () => { - fireEvent.mouseDown(selectElement); - }); - - // Wait for dropdown to populate with API data - await waitFor(() => { - expect( - screen.getByRole('option', { name: 'frontend' }), - ).toBeInTheDocument(); - }); - - expect(screen.getByRole('option', { name: 'backend' })).toBeInTheDocument(); - expect(screen.getByRole('option', { name: 'database' })).toBeInTheDocument(); - expect(screen.getByRole('option', { name: 'cache' })).toBeInTheDocument(); - - // Check if the first value 'frontend' is selected (active) - const frontendOption = screen.getByRole('option', { name: 'frontend' }); - expect(frontendOption).toHaveClass('active'); - }); - }); - - describe('Multi Select Default Values - ALL Enabled', () => { - it('should use default value when no previous selection exists', async () => { - const variableData: IDashboardVariable = { - id: 'var1', - name: 'services', - type: 'DYNAMIC', - multiSelect: true, - showALLOption: true, - defaultValue: ['backend', 'database'] as unknown as string, - selectedValue: undefined, - dynamicVariablesAttribute: 'service.name', - dynamicVariablesSource: 'Traces', - description: '', - sort: 'DISABLED', - allSelected: false, - }; - - await act(async () => { - renderWithQueryClient( - , - ); - }); - - expect(mockOnValueUpdate).toHaveBeenCalledWith( - 'services', - 'var1', - ['backend', 'database'], - true, - true, - ); - }); - - it('should preserve previous selection over default', async () => { - const variableData: IDashboardVariable = { - id: 'var1', - name: 'services', - type: 'DYNAMIC', - multiSelect: true, - showALLOption: true, - defaultValue: ['backend'] as unknown as string, - selectedValue: ['frontend', 'cache'], - dynamicVariablesAttribute: 'service.name', - dynamicVariablesSource: 'Traces', - description: '', - sort: 'DISABLED', - allSelected: false, - }; - - await act(async () => { - renderWithQueryClient( - , - ); - }); - - // Should NOT call onValueUpdate since previous selection exists - expect(mockOnValueUpdate).not.toHaveBeenCalledWith(); - - // The component shows individual selected values ['frontend', 'cache'] - // when specific values are selected (not ALL) - expect(screen.getByText('frontend')).toBeInTheDocument(); - expect(screen.getByText('cache')).toBeInTheDocument(); - - // Verify that the multi-select wrapper is present - const multiselectWrapper = screen - .getByTestId('variable-select') - .closest('.custom-multiselect-wrapper'); - expect(multiselectWrapper).toBeInTheDocument(); - - // Verify that 'backend' (default value) is NOT displayed since previous selection takes priority - expect(screen.queryByText('backend')).not.toBeInTheDocument(); - }); - - it('sahould default to ALL when no default and no previous selection', async () => { - const variableData: IDashboardVariable = { - id: 'var21', - name: 'services', - type: 'DYNAMIC', - multiSelect: true, - showALLOption: true, - defaultValue: undefined, - selectedValue: undefined, - dynamicVariablesAttribute: 'service.name', - dynamicVariablesSource: 'Traces', - description: '', - sort: 'DISABLED', - allSelected: false, - }; - - await act(async () => { - renderWithQueryClient( - , - ); - }); - - // Open dropdown to check if ALL option is selected - const selectElement = screen.getByRole('combobox'); - await act(async () => { - fireEvent.mouseDown(selectElement); - }); - - // Check if ALL option is present in dropdown - const allOption = screen.getByText('ALL'); - expect(allOption).toBeInTheDocument(); - - // Verify that the ALL option is available for selection - const allOptionContainer = allOption.closest( - '.option-item, .ant-select-item-option', - ); - expect(allOptionContainer).toBeInTheDocument(); - - // Check if the checkbox exists (it should be unchecked initially) - const checkbox = allOptionContainer?.querySelector( - '[role="checkbox"]', - ) as HTMLElement; - expect(checkbox).toBeInTheDocument(); - - // Should call onValueUpdate with all values (ALL selection) - expect(mockOnValueUpdate).toHaveBeenCalledWith( - 'services', - 'var21', - [], - true, // allSelected = true - false, - ); - }); - }); - - describe('Multi Select Default Values - ALL Disabled', () => { - it('should use default value over first value when provided', async () => { - const variableData: IDashboardVariable = { - id: 'var1', - name: 'services', - type: 'DYNAMIC', - multiSelect: true, - showALLOption: false, - defaultValue: ['database', 'cache'] as unknown as string, - selectedValue: undefined, - dynamicVariablesAttribute: 'service.name', - dynamicVariablesSource: 'Traces', - description: '', - sort: 'DISABLED', - allSelected: false, - }; - - await act(async () => { - renderWithQueryClient( - , - ); - }); - - // Should call onValueUpdate with default values - expect(mockOnValueUpdate).toHaveBeenCalledWith( - 'services', - 'var1', - ['database', 'cache'], - true, - true, - ); - }); - }); - - describe('ALL Option Special Value', () => { - it('should display ALL correctly when all values are selected', async () => { - const variableData: IDashboardVariable = { - id: 'var1', - name: 'services', - type: 'DYNAMIC', - multiSelect: true, - showALLOption: true, - allSelected: true, - selectedValue: ['frontend', 'backend', 'database', 'cache'], - dynamicVariablesAttribute: 'service.name', - dynamicVariablesSource: 'Traces', - description: '', - sort: 'DISABLED', - }; - - await act(async () => { - renderWithQueryClient( - , - ); - }); - - // Check if ALL is displayed in the UI (in the main selection area) - const allTextElement = screen.getByText('ALL'); - expect(allTextElement).toBeInTheDocument(); - - // Verify that the ALL selection wrapper is present and has correct class - const allSelectedWrapper = allTextElement.closest( - '.custom-multiselect-wrapper', - ); - expect(allSelectedWrapper).toHaveClass('all-selected'); - - // Open dropdown to check if ALL option is selected/active - const selectElement = screen.getByRole('combobox'); - await act(async () => { - fireEvent.mouseDown(selectElement); - }); - - // Wait for API data to be loaded and dropdown to populate - await waitFor(() => { - expect(getFieldValues).toHaveBeenCalledWith( - 'traces', - 'service.name', - '', - '2023-01-01T00:00:00Z', - '2023-01-02T00:00:00Z', - '', - undefined, // signal - ); - }); - - // Check if ALL option is present in dropdown and selected - // Use getAllByText to get all ALL elements and find the one in dropdown - const allElements = screen.getAllByText('ALL'); - expect(allElements.length).toBeGreaterThan(1); // Should have ALL in UI and dropdown - - // Find the ALL option in the dropdown (should have the 'all-option' class) - const dropdownAllOption = screen.getByRole('option', { name: 'ALL' }); - expect(dropdownAllOption).toBeInTheDocument(); - expect(dropdownAllOption).toHaveClass('all-option'); - expect(dropdownAllOption).toHaveClass('selected'); - - // Check if the checkbox for ALL option is checked - const checkbox = dropdownAllOption.querySelector( - '[role="checkbox"]', - ) as HTMLElement; - expect(checkbox).toBeInTheDocument(); - expect(checkbox).toHaveAttribute('data-state', 'checked'); - }); - }); -}); diff --git a/frontend/src/container/DashboardContainer/__test__/IntegrationTests.test.tsx b/frontend/src/container/DashboardContainer/__test__/IntegrationTests.test.tsx deleted file mode 100644 index 08ca8d0a48e..00000000000 --- a/frontend/src/container/DashboardContainer/__test__/IntegrationTests.test.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { QueryClient, QueryClientProvider } from 'react-query'; -// eslint-disable-next-line no-restricted-imports -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; - -// Import dependency building functions -import { - buildDependencies, - buildDependencyGraph, -} from '../DashboardVariablesSelection/util'; - -// Mock scrollIntoView since it's not available in JSDOM -window.HTMLElement.prototype.scrollIntoView = jest.fn(); - -function createMockStore(globalTime?: any): any { - return configureStore([])(() => ({ - globalTime: globalTime || { - minTime: '2023-01-01T00:00:00Z', - maxTime: '2023-01-02T00:00:00Z', - }, - })); -} - -// Mock the dashboard provider -const mockDashboard = { - data: { - variables: { - env: { - id: 'env', - name: 'env', - type: 'DYNAMIC', - selectedValue: 'production', - order: 1, - dynamicVariablesAttribute: 'environment', - dynamicVariablesSource: 'Traces', - }, - service: { - id: 'service', - name: 'service', - type: 'QUERY', - queryValue: 'SELECT DISTINCT service_name WHERE env = $env', - selectedValue: 'api-service', - order: 2, - }, - }, - }, -}; - -// Mock the dashboard provider with stable functions to prevent infinite loops -const mockSetDashboardData = jest.fn(); -const mockUpdateLocalStorageDashboardVariables = jest.fn(); -jest.mock('providers/Dashboard/store/useDashboardStore', () => ({ - useDashboardStore: (): any => ({ - dashboardData: mockDashboard, - setDashboardData: mockSetDashboardData, - updateLocalStorageDashboardVariables: - mockUpdateLocalStorageDashboardVariables, - }), -})); - -interface TestWrapperProps { - store: any; - children: React.ReactNode; -} - -function TestWrapper({ store, children }: TestWrapperProps): JSX.Element { - return ( - - - {children} - - - ); -} -TestWrapper.displayName = 'TestWrapper'; - -describe('Dynamic Variables Integration Tests', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Variable Dependencies and Updates', () => { - it('should build dependency graph correctly for variables', async () => { - // Convert mock dashboard variables to array format expected by the functions - const { variables } = mockDashboard.data as any; - const variablesArray = Object.values(variables) as any[]; - - // Test the actual dependency building logic - const dependencies = buildDependencies(variablesArray); - const dependencyData = buildDependencyGraph(dependencies); - - // Verify the dependency graph structure - expect(dependencies).toBeDefined(); - expect(dependencyData).toBeDefined(); - expect(dependencyData.order).toBeDefined(); - expect(dependencyData.graph).toBeDefined(); - expect(dependencyData.hasCycle).toBeDefined(); - - // Verify that service depends on env (based on queryValue containing $env) - // The dependencies object shows which variables depend on each variable - // So dependencies.env should contain 'service' because service references $env - expect(dependencies.env).toContain('service'); - - // Verify the topological order (env should come before service) - expect(dependencyData.order).toContain('env'); - expect(dependencyData.order).toContain('service'); - expect(dependencyData.order.indexOf('env')).toBeLessThan( - dependencyData.order.indexOf('service'), - ); - - // Verify no cycles in this simple case - expect(dependencyData.hasCycle).toBe(false); - }); - - it('should handle circular dependency detection', () => { - // Create variables with circular dependency - const circularVariables = [ - { - id: 'var1', - name: 'var1', - type: 'QUERY', - queryValue: 'SELECT * WHERE field = $var2', - order: 1, - }, - { - id: 'var2', - name: 'var2', - type: 'QUERY', - queryValue: 'SELECT * WHERE field = $var1', - order: 2, - }, - ]; - - // Test the actual circular dependency detection logic - const dependencies = buildDependencies(circularVariables as any); - const dependencyData = buildDependencyGraph(dependencies); - - // Verify that circular dependency is detected - expect(dependencyData.hasCycle).toBe(true); - expect(dependencyData.cycleNodes).toBeDefined(); - expect(dependencyData.cycleNodes).toContain('var1'); - expect(dependencyData.cycleNodes).toContain('var2'); - - // Verify the dependency structure - expect(dependencies.var1).toContain('var2'); - expect(dependencies.var2).toContain('var1'); - - // Verify that topological order is incomplete due to cycle - expect(dependencyData.order.length).toBeLessThan(2); - }); - }); -}); diff --git a/frontend/src/container/DashboardContainer/__test__/PanelManagement.test.tsx b/frontend/src/container/DashboardContainer/__test__/PanelManagement.test.tsx deleted file mode 100644 index 260e1bf4797..00000000000 --- a/frontend/src/container/DashboardContainer/__test__/PanelManagement.test.tsx +++ /dev/null @@ -1,508 +0,0 @@ -import { VirtuosoMockContext } from 'react-virtuoso'; -import { act, fireEvent, render, screen } from '@testing-library/react'; -import { renderHook } from '@testing-library/react'; -import { - Dashboard, - IDashboardVariable, - Widgets, -} from 'types/api/dashboard/getAll'; - -import { useAddDynamicVariableToPanels } from '../../../hooks/dashboard/useAddDynamicVariableToPanels'; -import { WidgetSelector } from '../DashboardSettings/DashboardVariableSettings/VariableItem/WidgetSelector'; - -// Mock scrollIntoView since it's not available in JSDOM -window.HTMLElement.prototype.scrollIntoView = jest.fn(); - -// Constants to avoid duplication -const CPU_USAGE_TEXT = 'CPU Usage'; -const MEMORY_USAGE_TEXT = 'Memory Usage'; -const ROW_WIDGET_TEXT = 'Row Widget'; - -// Helper function to create variable config -const createVariableConfig = ( - name: string, - attribute: string, -): IDashboardVariable => ({ - id: `var_${name}`, - name, - type: 'DYNAMIC', - description: '', - sort: 'DISABLED', - multiSelect: false, - showALLOption: false, - dynamicVariablesAttribute: attribute, -}); - -const mockDashboard = { - data: { - widgets: [ - { - id: 'widget1', - title: CPU_USAGE_TEXT, - panelTypes: 'GRAPH', - }, - { - id: 'widget2', - title: MEMORY_USAGE_TEXT, - panelTypes: 'TABLE', - }, - { - id: 'widget3', - title: ROW_WIDGET_TEXT, - panelTypes: 'row', // Should be filtered out (lowercase 'row' to match PANEL_GROUP_TYPES.ROW) - }, - ], - layout: [{ i: 'widget1' }, { i: 'widget2' }, { i: 'widget3' }], - }, -}; -// Mock dependencies -jest.mock('providers/Dashboard/store/useDashboardStore', () => ({ - useDashboardStore: (): any => ({ - dashboardData: mockDashboard, - }), -})); - -jest.mock('constants/queryBuilder', () => ({ - PANEL_GROUP_TYPES: { - ROW: 'row', // Match the actual constant value - }, - PANEL_TYPES: { - TIME_SERIES: 'graph', - VALUE: 'value', - TABLE: 'table', - LIST: 'list', - TRACE: 'trace', - BAR: 'bar', - PIE: 'pie', - HISTOGRAM: 'histogram', - EMPTY_WIDGET: 'EMPTY_WIDGET', - }, - initialQueriesMap: { - metrics: { - builder: { - queryData: [{}], - }, - }, - logs: { - builder: { - queryData: [{}], - }, - }, - traces: { - builder: { - queryData: [{}], - }, - }, - }, -})); - -jest.mock('container/GridPanelSwitch/utils', () => ({ - generateGridTitle: (title: string): string => title || 'Untitled Panel', -})); - -describe('Panel Management Tests', () => { - describe('WidgetSelector Component', () => { - const mockSetSelectedWidgets = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should display panel titles using generateGridTitle', async () => { - await act(async () => { - render( - - - , - , - ); - }); - - // Open the dropdown to see options - const selectElement = screen.getByRole('combobox'); - await act(async () => { - fireEvent.mouseDown(selectElement); - }); - - // Should show panel titles (excluding ROW widgets) in dropdown - expect(screen.getByText(CPU_USAGE_TEXT)).toBeInTheDocument(); - expect(screen.getByText(MEMORY_USAGE_TEXT)).toBeInTheDocument(); - expect(screen.queryByText(ROW_WIDGET_TEXT)).not.toBeInTheDocument(); - }); - - it('should filter out row widgets and widgets without layout', () => { - const modifiedDashboard = { - ...mockDashboard, - data: { - ...mockDashboard.data, - widgets: [ - ...mockDashboard.data.widgets, - { - id: 'widget4', - title: 'Orphaned Widget', - panelTypes: 'GRAPH', - }, // No layout entry - ], - }, - }; - - // Temporarily mock the dashboard - jest.doMock('providers/Dashboard/store/useDashboardStore', () => ({ - useDashboardStore: (): any => ({ - dashboardData: modifiedDashboard, - }), - })); - - render( - , - ); - - // Should not show orphaned widget - expect(screen.queryByText('Orphaned Widget')).not.toBeInTheDocument(); - }); - - it('should show selected widgets correctly', async () => { - await act(async () => { - render( - , - ); - }); - - // Component should show ALL text when all widgets are selected - const allElements = screen.getAllByText('ALL'); - expect(allElements.length).toBeGreaterThan(0); - - // Check if the dropdown shows selected state correctly - const selectElement = screen.getByRole('combobox'); - await act(async () => { - fireEvent.mouseDown(selectElement); - }); - - // Verify the ALL option in the dropdown is selected/checked - // Find the ALL option by its role instead of text to avoid ambiguity - const allOption = screen.getByRole('option', { name: /ALL/i }); - expect(allOption).toBeInTheDocument(); - expect(allOption).toHaveClass('selected'); - - const allCheckbox = allOption.querySelector( - '[role="checkbox"]', - ) as HTMLElement; - expect(allCheckbox).toBeInTheDocument(); - expect(allCheckbox).toHaveAttribute('data-state', 'checked'); - }); - }); - - describe('useAddDynamicVariableToPanels Hook', () => { - it('should add tag filters to specific selected panels', () => { - const { result } = renderHook(() => useAddDynamicVariableToPanels()); - const addDynamicVariableToPanels = result.current; - - const dashboard: Dashboard = { - data: { - widgets: [ - { - id: 'widget1', - query: { - builder: { - queryData: [ - { - filter: { expression: '' }, - filters: { items: [] }, - }, - ], - }, - }, - }, - ], - }, - } as any; - - const variableConfig = createVariableConfig('service', 'service.name'); - - const updatedDashboard = addDynamicVariableToPanels( - dashboard, - variableConfig, - ['widget1'], - false, - ); - - // Verify tag filter was added to the specific widget using new filter expression format - const widget = updatedDashboard?.data?.widgets?.[0] as any; - const queryData = widget?.query?.builder?.queryData?.[0]; - - // Check that filter expression contains the variable reference - expect(queryData.filter.expression).toContain('service.name in $service'); - - // Check that filters array also contains the filter item - const filters = queryData.filters.items; - expect(filters).toContainEqual( - expect.objectContaining({ - id: expect.any(String), // ID is generated dynamically - key: { - dataType: 'string', - id: 'service.name--string--', - key: 'service.name', - type: '', - }, - op: 'IN', - value: '$service', - }), - ); - }); - - it('should apply to all panels when applyToAll is true', () => { - const { result } = renderHook(() => useAddDynamicVariableToPanels()); - const addDynamicVariableToPanels = result.current; - - const dashboard: Dashboard = { - data: { - widgets: [ - { - id: 'widget1', - query: { - builder: { - queryData: [ - { - filter: { expression: '' }, - filters: { items: [] }, - }, - ], - }, - }, - }, - { - id: 'widget2', - query: { - builder: { - queryData: [ - { - filter: { expression: '' }, - filters: { items: [] }, - }, - ], - }, - }, - }, - ], - }, - } as any; - - const variableConfig = createVariableConfig('service', 'service.name'); - - const updatedDashboard = addDynamicVariableToPanels( - dashboard, - variableConfig, - ['widget1'], - true, // Apply to all - ); - - // Both widgets should have the filter expression - const widget1QueryData = (updatedDashboard?.data?.widgets?.[0] as Widgets) - ?.query?.builder?.queryData?.[0]; - const widget2QueryData = (updatedDashboard?.data?.widgets?.[1] as Widgets) - ?.query?.builder?.queryData?.[0]; - - // Check filter expressions - expect(widget1QueryData?.filter?.expression).toContain( - 'service.name in $service', - ); - expect(widget2QueryData?.filter?.expression).toContain( - 'service.name in $service', - ); - - // Check filters arrays - const widget1Filters = widget1QueryData?.filters?.items; - const widget2Filters = widget2QueryData?.filters?.items; - - expect(widget1Filters).toContainEqual( - expect.objectContaining({ - id: expect.any(String), // ID is generated dynamically - key: { - dataType: 'string', - id: 'service.name--string--', - key: 'service.name', - type: '', - }, - op: 'IN', - value: '$service', - }), - ); - expect(widget2Filters).toContainEqual( - expect.objectContaining({ - id: expect.any(String), - key: { - dataType: 'string', - id: 'service.name--string--', - key: 'service.name', - type: '', - }, - op: 'IN', - value: '$service', - }), - ); - }); - - it('should validate tag filter format with variable name', () => { - const { result } = renderHook(() => useAddDynamicVariableToPanels()); - const addDynamicVariableToPanels = result.current; - - const dashboard: Dashboard = { - data: { - widgets: [ - { - id: 'widget1', - query: { - builder: { - queryData: [ - { - filters: { items: [] }, - filter: { expression: '' }, - }, - ], - }, - }, - }, - ], - }, - } as any; - - const variableConfig = { - name: 'custom_service_var', - dynamicVariablesAttribute: 'service.name', - }; - - const updatedDashboard = addDynamicVariableToPanels( - dashboard, - variableConfig as any, - ['widget1'], - false, - ); - - const filters = (updatedDashboard?.data?.widgets?.[0] as Widgets)?.query - ?.builder?.queryData?.[0]?.filters?.items; - - const filterExpression = (updatedDashboard?.data?.widgets?.[0] as Widgets) - ?.query?.builder?.queryData?.[0]?.filter?.expression; - - expect(filterExpression).toContain('service.name in $custom_service_var'); - expect(filters).toContainEqual( - expect.objectContaining({ - value: '$custom_service_var', // Should use exact variable name - }), - ); - }); - - it('should handle empty widget selection gracefully', () => { - const { result } = renderHook(() => useAddDynamicVariableToPanels()); - const addDynamicVariableToPanels = result.current; - - const dashboard: Dashboard = { - data: { widgets: [] }, - } as any; - - const variableConfig = { - name: 'service', - dynamicVariablesAttribute: 'service.name', - }; - - const updatedDashboard = addDynamicVariableToPanels( - dashboard, - variableConfig as any, - [], - false, - ); - - // Should return dashboard unchanged - expect(updatedDashboard).toStrictEqual(dashboard); - }); - - it('should handle undefined dashboard gracefully', () => { - const { result } = renderHook(() => useAddDynamicVariableToPanels()); - const addDynamicVariableToPanels = result.current; - - const variableConfig = { - name: 'service', - dynamicVariablesAttribute: 'service.name', - }; - - const updatedDashboard = addDynamicVariableToPanels( - undefined, - variableConfig as any, - ['widget1'], - false, - ); - - // Should return undefined - expect(updatedDashboard).toBeUndefined(); - }); - - it('should preserve existing filters when adding new variable filter', () => { - const { result } = renderHook(() => useAddDynamicVariableToPanels()); - const addDynamicVariableToPanels = result.current; - - const dashboard: Dashboard = { - data: { - widgets: [ - { - id: 'widget1', - query: { - builder: { - queryData: [ - { - filter: { - expression: "service.name IN ['adservice']", - }, - }, - ], - }, - }, - }, - ], - }, - } as any; - - const variableConfig = { - name: 'host.name', - dynamicVariablesAttribute: 'host.name', - description: '', - type: 'DYNAMIC', - queryValue: '', - customValue: '', - textboxValue: '', - multiSelect: false, - showALLOption: false, - sort: 'DISABLED', - defaultValue: '3', - modificationUUID: 'bd3a85ab-1393-4783-971e-c252adfd4920', - id: '6b6f526a-6404-46fc-8d87-dc53e7ba8e1f', - order: 0, - dynamicVariablesSource: 'Traces', - }; - - const updatedDashboard = addDynamicVariableToPanels( - dashboard, - variableConfig as any, - ['widget1'], - false, - ); - - const filterExpression = (updatedDashboard?.data?.widgets?.[0] as Widgets) - ?.query?.builder?.queryData?.[0]?.filter?.expression; - - expect(filterExpression).toContain( - "service.name IN ['adservice'] host.name in $host.name", - ); - }); - }); -}); diff --git a/frontend/src/container/DashboardContainer/__test__/VariableItem.test.tsx b/frontend/src/container/DashboardContainer/__test__/VariableItem.test.tsx deleted file mode 100644 index b808006a845..00000000000 --- a/frontend/src/container/DashboardContainer/__test__/VariableItem.test.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { QueryClient, QueryClientProvider } from 'react-query'; -// eslint-disable-next-line no-restricted-imports -import { Provider } from 'react-redux'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import configureStore from 'redux-mock-store'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; - -import VariableItem from '../DashboardSettings/DashboardVariableSettings/VariableItem/VariableItem'; - -// Mock dependencies -jest.mock('api/dashboard/variables/dashboardVariablesQuery'); -jest.mock('hooks/dynamicVariables/useGetFieldValues', () => ({ - useGetFieldValues: (): any => ({ - data: { - payload: { - normalizedValues: ['frontend', 'backend', 'database'], - }, - }, - isLoading: false, - error: null, - }), -})); -jest.mock('components/Editor', () => { - function MockEditor({ - value, - onChange, - }: { - value: string; - onChange: (value: string) => void; - }): JSX.Element { - return ( -