diff --git a/.babelrc b/.babelrc deleted file mode 100644 index e21b3f2f2b..0000000000 --- a/.babelrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "presets": [ - [ - "@babel/preset-env", - { - "targets": { "node": "10" } - } - ], - "@babel/preset-react", - "@babel/preset-typescript" - ], - "plugins": [ - "@babel/plugin-transform-modules-commonjs", - ["@babel/plugin-transform-runtime", { "regenerator": true }], - "@babel/plugin-proposal-class-properties", - "@babel/plugin-proposal-object-rest-spread" - ] -} \ No newline at end of file diff --git a/.cypress/.eslintrc.js b/.cypress/.eslintrc.js new file mode 100644 index 0000000000..d38ee9d66c --- /dev/null +++ b/.cypress/.eslintrc.js @@ -0,0 +1,16 @@ +module.exports = { + root: true, + extends: ['plugin:cypress/recommended'], + env: { + 'cypress/globals': true, + }, + plugins: ['cypress'], + rules: { + // Add cypress specific rules here + 'cypress/no-assigning-return-values': 'error', + 'cypress/no-unnecessary-waiting': 'error', + 'cypress/assertion-before-screenshot': 'warn', + 'cypress/no-force': 'warn', + 'cypress/no-async-tests': 'error', + }, +}; diff --git a/.cypress/integration/integrations_test/integrations.spec.js b/.cypress/integration/integrations_test/integrations.spec.js index 4b446f26fa..f1281a795a 100644 --- a/.cypress/integration/integrations_test/integrations.spec.js +++ b/.cypress/integration/integrations_test/integrations.spec.js @@ -2,15 +2,14 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ +/* eslint-disable jest/expect-expect */ /// -import { - TEST_INTEGRATION_INSTANCE, TEST_SAMPLE_INSTANCE, -} from '../../utils/constants'; +import { TEST_INTEGRATION_INSTANCE, TEST_SAMPLE_INSTANCE } from '../../utils/constants'; -let testInstanceSuffix = (Math.random() + 1).toString(36).substring(7); -let testInstance = `${TEST_INTEGRATION_INSTANCE}_${testInstanceSuffix}`; +const testInstanceSuffix = (Math.random() + 1).toString(36).substring(7); +const testInstance = `${TEST_INTEGRATION_INSTANCE}_${testInstanceSuffix}`; const moveToIntegrationsHome = () => { cy.visit(`${Cypress.env('opensearchDashboards')}/app/integrations#/available`); @@ -28,10 +27,9 @@ const createSamples = () => { moveToAvailableNginxIntegration(); cy.get('[data-test-subj="try-it-button"]').click(); cy.get('.euiToastHeader__title').should('contain', 'successfully'); -} - +}; -describe('Basic sanity test for integrations plugin', () => { +describe('Integrations plugin', () => { it('Navigates to integrations plugin and expects the correct header', () => { moveToIntegrationsHome(); cy.get('[data-test-subj="integrations-header"]').should('exist'); @@ -40,66 +38,66 @@ describe('Basic sanity test for integrations plugin', () => { it('Navigates to integrations plugin and tests that clicking the nginx cards navigates to the nginx page', () => { moveToIntegrationsHome(); cy.get('[data-test-subj="integration_card_nginx"]').click(); - cy.url().should('include', '/available/nginx') - }) + cy.url().should('include', '/available/nginx'); + }); it('Navigates to nginx page and asserts the page to be as expected', () => { moveToAvailableNginxIntegration(); - cy.get('[data-test-subj="nginx-overview"]').should('exist') - cy.get('[data-test-subj="nginx-details"]').should('exist') - cy.get('[data-test-subj="nginx-screenshots"]').should('exist') - cy.get('[data-test-subj="nginx-assets"]').should('exist') + cy.get('[data-test-subj="nginx-overview"]').should('exist'); + cy.get('[data-test-subj="nginx-details"]').should('exist'); + cy.get('[data-test-subj="nginx-screenshots"]').should('exist'); + cy.get('[data-test-subj="nginx-assets"]').should('exist'); cy.get('[data-test-subj="fields"]').click(); - cy.get('[data-test-subj="nginx-fields"]').should('exist') - }) + cy.get('[data-test-subj="nginx-fields"]').should('exist'); + }); it('Uses the search of assets and fields tables', () => { moveToAvailableNginxIntegration(); cy.get('input[type="search"]').eq(0).focus().type('ss4o{enter}'); - cy.get('.euiTableRow').should('have.length', 1);//Filters correctly to the index pattern + cy.get('.euiTableRow').should('have.length', 1); //Filters correctly to the index pattern cy.get('[data-test-subj="fields"]').click(); - cy.get('input[type="search"]').eq(0).focus().clear().type('severity.observe') - cy.get('.euiTableRow').should('have.length', 2);//Filters correctly to the field name - }) + cy.get('input[type="search"]').eq(0).focus().clear().type('severity.observe'); + cy.get('.euiTableRow').should('have.length', 2); //Filters correctly to the field name + }); it('Uses the filter of assets table', () => { moveToAvailableNginxIntegration(); cy.get('.euiFilterGroup').trigger('mouseover').click(); cy.get('.euiFilterSelectItem').contains('visualization').click(); - cy.get('.euiTableRow').should('have.length', 6);//Filters correctly to visualization types - }) + cy.get('.euiTableRow').should('have.length', 6); //Filters correctly to visualization types + }); }); -describe('Tests the add nginx integration instance flow', () => { +describe('Add nginx integration instance flow', () => { it('Navigates to nginx page and triggers the adds the instance flow', () => { createSamples(); moveToAvailableNginxIntegration(); cy.get('[data-test-subj="add-integration-button"]').click(); cy.get('[data-test-subj="new-instance-name"]').should('have.value', 'nginx Integration'); - cy.get('[data-test-subj="create-instance-button"]').should('be.disabled') + cy.get('[data-test-subj="create-instance-button"]').should('be.disabled'); // Modifies the name of the integration cy.get('[data-test-subj="new-instance-name"]').clear().type(testInstance); // Validates the created sample index cy.get('[data-test-subj="data-source-name"]').type('ss4o_logs-nginx-sample-sample{enter}'); cy.get('[data-test-subj="create-instance-button"]').click(); cy.get('[data-test-subj="eventHomePageTitle"]').should('contain', 'nginx'); - }) + }); it('Navigates to installed integrations page and verifies that nginx-test exists', () => { moveToAddedIntegrations(); cy.contains(testInstance).should('exist'); cy.get('input[type="search"]').eq(0).focus().type(`${testInstance}{enter}`); - cy.get('.euiTableRow').should('have.length', 1);//Filters correctly to the test integration instance + cy.get('.euiTableRow').should('have.length', 1); //Filters correctly to the test integration instance cy.get(`[data-test-subj="${testInstance}IntegrationLink"]`).click(); - }) + }); it('Navigates to added integrations page and verifies that nginx-test exists and linked asset works as expected', () => { moveToAddedIntegrations(); cy.contains(TEST_INTEGRATION_INSTANCE).should('exist'); cy.get(`[data-test-subj="${testInstance}IntegrationLink"]`).click(); cy.get(`[data-test-subj="IntegrationAssetLink"]`).click(); - cy.url().should('include', '/dashboards#/') - }) + cy.url().should('include', '/dashboards#/'); + }); it('Navigates to installed nginx-test instance page and deletes it', () => { moveToAddedIntegrations(); @@ -115,17 +113,15 @@ describe('Tests the add nginx integration instance flow', () => { cy.get('button[data-test-subj="popoverModal__deleteButton"]').should('not.be.disabled'); cy.get('button[data-test-subj="popoverModal__deleteButton"]').click(); cy.get('.euiToastHeader__title').should('contain', 'successfully'); - }) + }); }); -describe('Tests the add nginx integration instance flow', () => { +describe('Nginx try it flow', () => { it('Navigates to nginx page and triggers the try it flow', () => { moveToAvailableNginxIntegration(); cy.get('[data-test-subj="try-it-button"]').click(); cy.get('.euiToastHeader__title').should('contain', 'successfully'); moveToAddedIntegrations(); cy.contains(TEST_SAMPLE_INSTANCE).should('exist'); - }) + }); }); - - diff --git a/.eslintrc.js b/.eslintrc.js index 6ba689e332..12f093d2ed 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,18 +14,19 @@ module.exports = { '@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended', 'plugin:react-hooks/recommended', - "eslint:recommended", - "plugin:cypress/recommended", - "plugin:import/recommended", - "prettier" - ], - env: { - 'cypress/globals': true, - }, - plugins: [ - 'cypress', + 'plugin:jest/recommended', + 'plugin:prettier/recommended', ], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], '@osd/eslint/no-restricted-paths': [ 'error', { @@ -38,17 +39,12 @@ module.exports = { ], }, ], - // Add cypress specific rules here - 'cypress/no-assigning-return-values': 'error', - 'cypress/no-unnecessary-waiting': 'error', - 'cypress/assertion-before-screenshot': 'warn', - 'cypress/no-force': 'warn', - 'cypress/no-async-tests': 'error', }, overrides: [ { files: ['**/*.{js,ts,tsx}'], rules: { + '@typescript-eslint/no-explicit-any': 'warn', 'no-console': 0, '@osd/eslint/require-license-header': [ 'error', diff --git a/.github/draft-release-notes-config.yml b/.github/draft-release-notes-config.yml index 371f1b065e..34b199cbf7 100644 --- a/.github/draft-release-notes-config.yml +++ b/.github/draft-release-notes-config.yml @@ -16,30 +16,23 @@ replacers: categories: - title: 'Breaking Changes' labels: - - 'Breaking Changes' + - 'breaking' - title: 'Features' labels: - 'feature' - - title: 'Enhancements' - labels: - 'enhancement' - title: 'Bug Fixes' labels: - 'bug' - title: 'Infrastructure' labels: - - 'infra' - - 'test' - - 'dependencies' - - 'github actions' + - 'infrastructure' + - 'testing' + - 'integ-test-failure' + - 'repository' - title: 'Documentation' labels: - 'documentation' - title: 'Maintenance' labels: - - "version compatibility" - "maintenance" - - title: 'Refactoring' - labels: - - 'refactor' - - 'code quality' diff --git a/.github/workflows/enforce-labels.yml b/.github/workflows/enforce-labels.yml new file mode 100644 index 0000000000..71d923f66d --- /dev/null +++ b/.github/workflows/enforce-labels.yml @@ -0,0 +1,13 @@ +name: Enforce PR labels + +on: + pull_request: + types: [labeled, unlabeled, opened, edited, synchronize] +jobs: + enforce-label: + runs-on: ubuntu-latest + steps: + - uses: yogevbd/enforce-label-action@2.1.0 + with: + REQUIRED_LABELS_ANY: "breaking,feature,enhancement,bug,infrastructure,dependencies,documentation,maintenance,skip-changelog" + REQUIRED_LABELS_ANY_DESCRIPTION: "A release label is required: ['breaking', 'bug', 'dependencies', 'documentation', 'enhancement', 'feature', 'infrastructure', 'maintenance', 'skip-changelog']" diff --git a/.github/workflows/ftr-e2e-dashboards-observability-test.yml b/.github/workflows/ftr-e2e-dashboards-observability-test.yml new file mode 100644 index 0000000000..2e8a133d01 --- /dev/null +++ b/.github/workflows/ftr-e2e-dashboards-observability-test.yml @@ -0,0 +1,163 @@ +name: FTR E2E Dashboards observability Test + +on: [pull_request, push] + +env: + PLUGIN_NAME: dashboards-observability + OPENSEARCH_DASHBOARDS_VERSION: 'main' + OPENSEARCH_VERSION: '3.0.0' + OPENSEARCH_PLUGIN_VERSION: '3.0.0.0' + +jobs: + tests: + name: Run FTR E2E Dashboards Observability Tests + env: + # Prevents extra Cypress installation progress messages + CI: 1 + # Avoid warnings like "tput: No value for $TERM and no -T specified" + TERM: xterm + WORKING_DIR: ${{ matrix.working_directory }}. + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + java: [11] + runs-on: ${{ matrix.os }} + + steps: + - name: Set up Java 11 + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: '11' + + - name: Download observability artifact + uses: suisei-cn/actions-download-file@v1.4.0 + with: + url: https://aws.oss.sonatype.org/service/local/artifact/maven/redirect?r=snapshots&g=org.opensearch.plugin&a=opensearch-observability&v=${{ env.OPENSEARCH_PLUGIN_VERSION }}-SNAPSHOT&p=zip + target: plugin-artifacts/ + filename: observability.zip + + - name: Download SQL artifact + uses: suisei-cn/actions-download-file@v1.4.0 + with: + url: https://aws.oss.sonatype.org/service/local/artifact/maven/redirect?r=snapshots&g=org.opensearch.plugin&a=opensearch-sql-plugin&v=${{ env.OPENSEARCH_PLUGIN_VERSION }}-SNAPSHOT&p=zip + target: plugin-artifacts/ + filename: sql.zip + + - name: Download OpenSearch + uses: peternied/download-file@v2 + with: + url: https://artifacts.opensearch.org/snapshots/core/opensearch/${{ env.OPENSEARCH_VERSION }}-SNAPSHOT/opensearch-min-${{ env.OPENSEARCH_VERSION }}-SNAPSHOT-linux-x64-latest.tar.gz + + - name: Extract OpenSearch + run: | + tar -xzf opensearch-*.tar.gz + rm -f opensearch-*.tar.gz + shell: bash + + - name: Install observability plugin + run: | + /bin/bash -c "yes | ./opensearch-${{ env.OPENSEARCH_VERSION }}-SNAPSHOT/bin/opensearch-plugin install file:$(pwd)/plugin-artifacts/observability.zip" + shell: bash + + - name: Install SQL plugin + run: | + /bin/bash -c "yes | ./opensearch-${{ env.OPENSEARCH_VERSION }}-SNAPSHOT/bin/opensearch-plugin install file:$(pwd)/plugin-artifacts/sql.zip" + shell: bash + + - name: Run OpenSearch + run: /bin/bash -c "./opensearch-${{ env.OPENSEARCH_VERSION }}-SNAPSHOT/bin/opensearch &" + shell: bash + + - name: Checkout OpenSearch Dashboards + uses: actions/checkout@v2 + with: + repository: opensearch-project/Opensearch-Dashboards + ref: ${{ env.OPENSEARCH_DASHBOARDS_VERSION }} + path: OpenSearch-Dashboards + + - name: Checkout dashboards observability + uses: actions/checkout@v2 + with: + path: OpenSearch-Dashboards/plugins/dashboards-observability + + - name: Get node and yarn versions + working-directory: ${{ env.WORKING_DIR }} + id: versions_step + run: | + echo "::set-output name=node_version::$(cat ./OpenSearch-Dashboards/.nvmrc | cut -d"." -f1)" + echo "::set-output name=yarn_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.yarn).match(/[.0-9]+/)[0]")" + + - name: Setup node + uses: actions/setup-node@v1 + with: + node-version: ${{ steps.versions_step.outputs.node_version }} + registry-url: 'https://registry.npmjs.org' + + - name: Install correct yarn version for OpenSearch Dashboards + run: | + npm uninstall -g yarn + echo "Installing yarn ${{ steps.versions_step.outputs.yarn_version }}" + npm i -g yarn@${{ steps.versions_step.outputs.yarn_version }} + + - name: Bootstrap the plugin + run: | + cd OpenSearch-Dashboards/plugins/dashboards-observability + yarn osd bootstrap + + - name: Run OpenSearch Dashboards server + run: | + cd OpenSearch-Dashboards + nohup yarn start --no-base-path --no-watch | tee dashboard.log & + + - name : Check If OpenSearch Dashboards Is Ready + if: ${{ runner.os == 'Linux' }} + run: | + cd ./OpenSearch-Dashboards + if timeout 600 grep -q "bundles compiled successfully after" <(tail -n0 -f dashboard.log); then + echo "OpenSearch Dashboards compiled successfully." + else + echo "Timeout for 600 seconds reached. OpenSearch Dashboards did not finish compiling." + exit 1 + fi + + - name: Checkout Dashboards Functioanl Test Repo + uses: actions/checkout@v2 + with: + path: opensearch-dashboards-functional-test + repository: opensearch-project/opensearch-dashboards-functional-test + ref: ${{ env.OPENSEARCH_DASHBOARDS_VERSION }} + fetch-depth: 0 + + - name: Install Cypress + run: | + npm install cypress --save-dev + shell: bash + working-directory: opensearch-dashboards-functional-test + + - name: Get Cypress version + id: cypress_version + run: | + echo "::set-output name=cypress_version::$(cat ./package.json | jq '.dependencies.cypress' | tr -d '"')" + working-directory: opensearch-dashboards-functional-test + + - name: Run Cypress tests + run: | + yarn cypress:run-without-security --browser chromium --spec 'cypress/integration/plugins/observability-dashboards/*.js' + working-directory: opensearch-dashboards-functional-test + + - name: Capture failure screenshots + uses: actions/upload-artifact@v1 + if: failure() + with: + name: cypress-screenshots-${{ matrix.os }} + path: opensearch-dashboards-functional-test/cypress/screenshots + + - name: Capture failure test video + uses: actions/upload-artifact@v1 + if: failure() + with: + name: cypress-videos-${{ matrix.os }} + path: opensearch-dashboards-functional-test/cypress/videos diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d66abe5366..f26802c597 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,10 +1,10 @@ name: Lint -on: [push, pull_request] +on: [pull_request] env: PLUGIN_NAME: dashboards-observability - OPENSEARCH_DASHBOARDS_VERSION: 'main' + OPENSEARCH_DASHBOARDS_VERSION: "main" jobs: build: @@ -22,7 +22,8 @@ jobs: - name: Checkout dashboards observability uses: actions/checkout@v2 with: - path: OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }} + path: OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }} + fetch-depth: 0 - name: Get node and yarn versions working-directory: ${{ env.WORKING_DIR }} @@ -35,7 +36,7 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ steps.versions_step.outputs.node_version }} - registry-url: 'https://registry.npmjs.org' + registry-url: "https://registry.npmjs.org" - name: Install correct yarn version for OpenSearch Dashboards run: | @@ -44,18 +45,28 @@ jobs: npm i -g yarn@${{ steps.versions_step.outputs.yarn_version }} - name: Bootstrap the plugin - working-directory: OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }} - run: - yarn osd bootstrap + working-directory: OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }} + run: yarn osd bootstrap - - name: lint code base + - name: Get list of changed files + id: files + run: | + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + git fetch origin $BASE_SHA + git diff --name-only $BASE_SHA...$HEAD_SHA > changed_files.txt + CHANGED_FILES=$(cat changed_files.txt | grep -E '\.(js|ts|tsx)$' || true) + echo "::set-output name=changed::${CHANGED_FILES}" working-directory: OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }} + + - name: Lint Changed Files run: | - git fetch origin main - CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRTUXB origin/main | grep -E "\.(js|ts|tsx)$") - if [ -n "$CHANGED_FILES" ]; then + CHANGED_FILES="${{ steps.files.outputs.changed }}" + if [[ -n "$CHANGED_FILES" ]]; then echo "Linting changed files..." - yarn lint $CHANGED_FILES + IFS=$'\n' read -r -a FILES_TO_LINT <<< "$CHANGED_FILES" + yarn lint "${FILES_TO_LINT[@]}" else - echo "No JavaScript/TypeScript files changed." - fi \ No newline at end of file + echo "No matched files to lint." + fi + working-directory: OpenSearch-Dashboards/plugins/${{ env.PLUGIN_NAME }} diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000000..3805f7cf5d --- /dev/null +++ b/babel.config.js @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// babelrc doesn't respect NODE_PATH anymore but using require does. +// Alternative to install them locally in node_modules +module.exports = function (api) { + // ensure env is test so that this config won't impact build or dev server + if (api.env('test')) { + return { + presets: [ + require('@babel/preset-env'), + require('@babel/preset-react'), + require('@babel/preset-typescript'), + ], + plugins: [ + [require('@babel/plugin-transform-runtime'), { regenerator: true }], + require('@babel/plugin-transform-class-properties'), + require('@babel/plugin-transform-object-rest-spread'), + [require('@babel/plugin-transform-modules-commonjs'), { allowTopLevelThis: true }], + ], + }; + } + return {}; +}; diff --git a/common/constants/explorer.ts b/common/constants/explorer.ts index b6c2051259..d94957c3e8 100644 --- a/common/constants/explorer.ts +++ b/common/constants/explorer.ts @@ -9,7 +9,7 @@ import { VIS_CHART_TYPES } from './shared'; // URLs export const EVENT_ANALYTICS_DOCUMENTATION_URL = - 'https://opensearch.org/docs/latest/observability-plugin/event-analytics/'; + 'https://opensearch.org/docs/latest/observing-your-data/event-analytics/'; export const OPEN_TELEMETRY_LOG_CORRELATION_LINK = 'https://opentelemetry.io/docs/reference/specification/logs/overview/#log-correlation'; export const LOG_EXPLORER_BASE_PATH = 'observability-logs#/explorer/'; diff --git a/common/constants/notebooks.ts b/common/constants/notebooks.ts index 2ece3c29e7..c5eb07348c 100644 --- a/common/constants/notebooks.ts +++ b/common/constants/notebooks.ts @@ -4,7 +4,6 @@ */ export const NOTEBOOKS_API_PREFIX = '/api/observability/notebooks'; -export const NOTEBOOKS_SELECTED_BACKEND: 'ZEPPELIN' | 'DEFAULT' = 'DEFAULT'; export const NOTEBOOKS_FETCH_SIZE = 1000; export const CREATE_NOTE_MESSAGE = 'Enter a name to describe the purpose of this notebook.'; export const NOTEBOOKS_DOCUMENTATION_URL = diff --git a/package.json b/package.json index 3929fcd37d..079c09def7 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "performance-now": "^2.1.0", "plotly.js-dist": "^2.2.0", "postinstall": "^0.7.4", - "react-graph-vis": "^1.0.5", + "react-graph-vis": "^1.0.7", "react-paginate": "^8.1.3", "react-plotly.js": "^2.5.1", "redux-persist": "^6.0.0", @@ -50,16 +50,15 @@ "@types/enzyme-adapter-react-16": "^1.0.6", "@types/mime": "^3.0.1", "@types/react-plotly.js": "^2.5.0", - "@types/react-test-renderer": "^16.9.1", + "@types/react-test-renderer": "^18.0.0", "@types/sanitize-filename": "^1.6.3", "antlr4ts-cli": "^0.5.0-alpha.4", - "cypress": "^12.8.1", + "cypress": "^13.6.0", "cypress-watch-and-reload": "^1.10.6", "eslint": "^6.8.0", "husky": "^8.0.3", "jest-dom": "^4.0.0", - "lint-staged": "^13.1.0", - "ts-jest": "^29.1.0" + "lint-staged": "^13.1.0" }, "resolutions": { "react-syntax-highlighter": "^15.4.3", diff --git a/public/components/common/query_utils/__tests__/query_utils.test.tsx b/public/components/common/query_utils/__tests__/query_utils.test.tsx index 2db7db97d3..4119da5604 100644 --- a/public/components/common/query_utils/__tests__/query_utils.test.tsx +++ b/public/components/common/query_utils/__tests__/query_utils.test.tsx @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; import { + convertDateTime, findMinInterval, parsePromQLIntoKeywords, preprocessMetricQuery, @@ -61,6 +61,42 @@ describe('Query Utils', () => { expect(minInterval).toEqual(span); }); }); + describe('convertDateTime', () => { + it('converts from absolute timestamp', () => { + const time = '2020-07-21T18:37:44.710Z'; + const converted = convertDateTime(time); + expect(converted).toEqual('2020-07-21 18:37:44.710000'); + }); + it('formats to PPL standard format when default formatting', () => { + const time = '2020-07-21T18:37:44.710Z'; + const converted = convertDateTime(time, true, true); + expect(converted).toEqual('2020-07-21 18:37:44.710000'); + }); + it('formats to specified format when provided', () => { + const time = '2020-07-21T18:37:44.710Z'; + const converted = convertDateTime(time, true, 'YYYY-MMM-DD'); + expect(converted).toMatch(/2020-jul-21/i); + }); + describe('with moment reference notations', () => { + beforeEach(() => { + jest.useFakeTimers().setSystemTime(new Date('2020-02-02 12:01:00')); + }); + afterEach(() => { + jest.useRealTimers(); + }); + + it('converts named-reference, rounded', () => { + const time = 'now-1d/d'; + const converted = convertDateTime(time, true); + expect(converted).toEqual('2020-02-01 00:00:00.000000'); + }); + it.skip('converts named-reference, rounded as end of interval', () => { + const time = 'now/d'; + const converted = convertDateTime(time); + expect(converted).toEqual('2020-02-02 23:59:59.999999'); + }); + }); + }); describe('Metric Query processors', () => { const defaultQueryMetaData = { catalogSourceName: 'my_catalog', diff --git a/public/components/common/query_utils/index.ts b/public/components/common/query_utils/index.ts index 489afad130..eccd6b9ddd 100644 --- a/public/components/common/query_utils/index.ts +++ b/public/components/common/query_utils/index.ts @@ -74,7 +74,9 @@ export const convertDateTime = ( const epochTime = myDate.getTime() / 1000.0; return Math.round(epochTime); } - if (formatted) return returnTime!.utc().format(PPL_DATE_FORMAT); + if (formatted === true) return returnTime?.utc()?.format(PPL_DATE_FORMAT); + if (formatted) return returnTime?.utc()?.format(formatted); + return returnTime; }; diff --git a/public/components/custom_panels/helpers/utils.tsx b/public/components/custom_panels/helpers/utils.tsx index 9abc0d8d7b..e06c795df7 100644 --- a/public/components/custom_panels/helpers/utils.tsx +++ b/public/components/custom_panels/helpers/utils.tsx @@ -259,25 +259,34 @@ const dynamicLayoutFromQueryData = (queryData) => { }; }; -const createCatalogVisualizationMetaData = ( - catalogSource: string, - visualizationQuery: string, - visualizationType: string, - visualizationTimeField: string, - queryData: object -) => { +const createCatalogVisualizationMetaData = ({ + catalogSource, + query, + type, + subType, + timeField, + queryData, +}: { + catalogSource: string; + query: string; + type: string; + subType: string; + timeField: string; + queryData: object; +}) => { return { name: catalogSource, description: '', - query: visualizationQuery, - type: visualizationType, + query, + type, + subType, selected_date_range: { start: 'now/y', end: 'now', text: '', }, selected_timestamp: { - name: visualizationTimeField, + name: timeField, type: 'timestamp', }, selected_fields: { @@ -332,6 +341,7 @@ export const renderCatalogVisualization = async ({ const visualizationType = 'line'; const visualizationTimeField = '@timestamp'; + const visualizationSubType = visualization.subType; const visualizationQuery = updateCatalogVisualizationQuery({ ...visualization.queryMetaData, @@ -357,15 +367,15 @@ export const renderCatalogVisualization = async ({ ); setVisualizationData(queryData); - const visualizationMetaData = createCatalogVisualizationMetaData( + const visualizationMetaData = createCatalogVisualizationMetaData({ catalogSource, - visualizationQuery, - visualizationType, - visualizationTimeField, - queryData - ); + query: visualizationQuery, + type: visualizationType, + subType: visualization.subType, + timeField: visualizationTimeField, + queryData, + }); - console.log('renderCatalogVisualization', { visualizationMetaData }); setVisualizationMetaData(visualizationMetaData); } catch (error) { setIsError({ error }); diff --git a/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx b/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx index 3d52f9eddd..6cc87f8e02 100644 --- a/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx +++ b/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx @@ -37,7 +37,10 @@ import './visualization_container.scss'; import { VizContainerError } from '../../../../../common/types/custom_panels'; import { metricQuerySelector } from '../../../metrics/redux/slices/metrics_slice'; import { coreRefs } from '../../../../framework/core_refs'; -import { PROMQL_METRIC_SUBTYPE } from '../../../../../common/constants/shared'; +import { + PROMQL_METRIC_SUBTYPE, + observabilityMetricsID, +} from '../../../../../common/constants/shared'; /* * Visualization container - This module is a placeholder to add visualizations in react-grid-layout @@ -78,6 +81,7 @@ interface Props { removeVisualization?: (visualizationId: string) => void; catalogVisualization?: boolean; inlineEditor?: JSX.Element; + actionMenuType?: string; } export const VisualizationContainer = ({ @@ -98,6 +102,7 @@ export const VisualizationContainer = ({ removeVisualization, catalogVisualization, inlineEditor, + actionMenuType, }: Props) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [visualizationTitle, setVisualizationTitle] = useState(''); @@ -175,7 +180,11 @@ export const VisualizationContainer = ({ disabled={editMode} onClick={() => { closeActionsMenu(); - onEditClick(savedVisualizationId); + if (visualizationMetaData?.subType === PROMQL_METRIC_SUBTYPE) { + window.location.assign(`${observabilityMetricsID}#/${savedVisualizationId}`); + } else { + onEditClick(savedVisualizationId); + } }} > Edit @@ -217,7 +226,10 @@ export const VisualizationContainer = ({ , ]; - if (visualizationMetaData?.subType === PROMQL_METRIC_SUBTYPE) { + if ( + visualizationMetaData?.subType === PROMQL_METRIC_SUBTYPE && + actionMenuType === 'metricsGrid' + ) { popoverPanel = [showPPLQueryPanel]; } else if (usedInNotebooks) { popoverPanel = [popoverPanel[0]]; diff --git a/public/components/integrations/components/__tests__/create_integration_helpers.test.ts b/public/components/integrations/components/__tests__/create_integration_helpers.test.ts index 71ccc99bf6..2bf7c76663 100644 --- a/public/components/integrations/components/__tests__/create_integration_helpers.test.ts +++ b/public/components/integrations/components/__tests__/create_integration_helpers.test.ts @@ -3,17 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { HttpResponse, HttpSetup } from '../../../../../../../src/core/public'; +import { coreStartMock } from '../../../../../test/__mocks__/coreMocks'; import { checkDataSourceName, - doTypeValidation, + doExistingDataSourceValidation, doNestedPropertyValidation, doPropertyValidation, + doTypeValidation, fetchDataSourceMappings, fetchIntegrationMappings, - doExistingDataSourceValidation, } from '../create_integration_helpers'; -import * as create_integration_helpers from '../create_integration_helpers'; -import { HttpSetup } from '../../../../../../../src/core/public'; describe('doTypeValidation', () => { it('should return true if required type is not specified', () => { @@ -261,73 +261,88 @@ describe('fetchIntegrationMappings', () => { describe('doExistingDataSourceValidation', () => { it('Catches and returns checkDataSourceName errors', async () => { - const mockHttp = {} as Partial; - jest - .spyOn(create_integration_helpers, 'checkDataSourceName') - .mockReturnValue({ ok: false, errors: ['mock'] }); - - const result = doExistingDataSourceValidation('target', 'name', 'type', mockHttp as HttpSetup); - - await expect(result).resolves.toHaveProperty('ok', false); + coreStartMock.http.post = jest.fn(() => + Promise.resolve(({ + logs: { mappings: { properties: { test: true } } }, + } as unknown) as HttpResponse) + ); + coreStartMock.http.get = jest.fn(() => + Promise.resolve(({ + data: { mappings: { logs: { template: { mappings: { properties: { test: true } } } } } }, + statusCode: 200, + } as unknown) as HttpResponse) + ); + + const result = await doExistingDataSourceValidation('ss4o_metrics-test-test', 'target', 'logs'); + + expect(result).toHaveProperty('ok', false); }); it('Catches data stream fetch errors', async () => { - const mockHttp = {} as Partial; - jest.spyOn(create_integration_helpers, 'checkDataSourceName').mockReturnValue({ ok: true }); - jest.spyOn(create_integration_helpers, 'fetchDataSourceMappings').mockResolvedValue(null); - jest - .spyOn(create_integration_helpers, 'fetchIntegrationMappings') - .mockResolvedValue({ test: { template: { mappings: {} } } }); - - const result = doExistingDataSourceValidation('target', 'name', 'type', mockHttp as HttpSetup); - - await expect(result).resolves.toHaveProperty('ok', false); + coreStartMock.http.post = jest.fn(() => + Promise.resolve(({ + mock: 'resp', + } as unknown) as HttpResponse) + ); + + coreStartMock.http.get = jest.fn(() => + Promise.resolve(({ + data: { mappings: { logs: { template: { mappings: { properties: { test: true } } } } } }, + statusCode: 200, + } as unknown) as HttpResponse) + ); + + const result = await doExistingDataSourceValidation('ss4o_logs-test-test', 'target', 'logs'); + expect(result).toHaveProperty('ok', false); }); it('Catches integration fetch errors', async () => { - const mockHttp = {} as Partial; - jest.spyOn(create_integration_helpers, 'checkDataSourceName').mockReturnValue({ ok: true }); - jest - .spyOn(create_integration_helpers, 'fetchDataSourceMappings') - .mockResolvedValue({ test: { properties: {} } }); - jest.spyOn(create_integration_helpers, 'fetchIntegrationMappings').mockResolvedValue(null); - - const result = doExistingDataSourceValidation('target', 'name', 'type', mockHttp as HttpSetup); - - await expect(result).resolves.toHaveProperty('ok', false); + coreStartMock.http.post = jest.fn(() => + Promise.resolve(({ + logs: { mappings: { properties: { test: true } } }, + } as unknown) as HttpResponse) + ); + coreStartMock.http.get = jest.fn(() => + Promise.resolve(({ + mock: 'resp', + } as unknown) as HttpResponse) + ); + + const result = await doExistingDataSourceValidation('ss4o_logs-test-test', 'target', 'logs'); + expect(result).toHaveProperty('ok', false); }); it('Catches type validation issues', async () => { - const mockHttp = {} as Partial; - jest.spyOn(create_integration_helpers, 'checkDataSourceName').mockReturnValue({ ok: true }); - jest - .spyOn(create_integration_helpers, 'fetchDataSourceMappings') - .mockResolvedValue({ test: { properties: {} } }); - jest - .spyOn(create_integration_helpers, 'fetchIntegrationMappings') - .mockResolvedValue({ test: { template: { mappings: {} } } }); - jest - .spyOn(create_integration_helpers, 'doPropertyValidation') - .mockReturnValue({ ok: false, errors: ['mock'] }); - - const result = doExistingDataSourceValidation('target', 'name', 'type', mockHttp as HttpSetup); - - await expect(result).resolves.toHaveProperty('ok', false); + coreStartMock.http.post = jest.fn(() => + Promise.resolve(({ + logs: { mappings: { properties: { test: true } } }, + } as unknown) as HttpResponse) + ); + coreStartMock.http.get = jest.fn(() => + Promise.resolve(({ + data: { mappings: { template: { mappings: { properties: { test: true } } } } }, + statusCode: 200, + } as unknown) as HttpResponse) + ); + + const result = await doExistingDataSourceValidation('ss4o_logs-test-test', 'target', 'logs'); + expect(result).toHaveProperty('ok', false); }); it('Returns no errors if everything passes', async () => { - const mockHttp = {} as Partial; - jest.spyOn(create_integration_helpers, 'checkDataSourceName').mockReturnValue({ ok: true }); - jest - .spyOn(create_integration_helpers, 'fetchDataSourceMappings') - .mockResolvedValue({ test: { properties: {} } }); - jest - .spyOn(create_integration_helpers, 'fetchIntegrationMappings') - .mockResolvedValue({ test: { template: { mappings: {} } } }); - jest.spyOn(create_integration_helpers, 'doPropertyValidation').mockReturnValue({ ok: true }); - - const result = doExistingDataSourceValidation('target', 'name', 'type', mockHttp as HttpSetup); - - await expect(result).resolves.toHaveProperty('ok', true); + coreStartMock.http.post = jest.fn(() => + Promise.resolve(({ + logs: { mappings: { properties: { test: true } } }, + } as unknown) as HttpResponse) + ); + coreStartMock.http.get = jest.fn(() => + Promise.resolve(({ + data: { mappings: { logs: { template: { mappings: { properties: { test: true } } } } } }, + statusCode: 200, + } as unknown) as HttpResponse) + ); + + const result = await doExistingDataSourceValidation('ss4o_logs-test-test', 'target', 'logs'); + expect(result).toHaveProperty('ok', true); }); }); diff --git a/public/components/integrations/components/setup_integration.tsx b/public/components/integrations/components/setup_integration.tsx index 8b7efea453..97fc85e02d 100644 --- a/public/components/integrations/components/setup_integration.tsx +++ b/public/components/integrations/components/setup_integration.tsx @@ -26,11 +26,14 @@ import { EuiTitle, } from '@elastic/eui'; import React, { useState, useEffect } from 'react'; -import { Color } from 'common/constants/integrations'; +import { Color } from '../../../../common/constants/integrations'; import { coreRefs } from '../../../framework/core_refs'; import { IntegrationTemplate, addIntegrationRequest } from './create_integration_helpers'; -import { CONSOLE_PROXY, INTEGRATIONS_BASE } from '../../../../common/constants/shared'; -import { DATACONNECTIONS_BASE } from '../../../../common/constants/shared'; +import { + CONSOLE_PROXY, + INTEGRATIONS_BASE, + DATACONNECTIONS_BASE, +} from '../../../../common/constants/shared'; export interface IntegrationSetupInputs { displayName: string; @@ -119,7 +122,7 @@ const suggestDataSources = async (type: string): Promise (dispatch, getState) => { + const { metrics } = getState().metrics; + const modifiableMetricsMap = { ...metrics }; -const now = () => new Date().getMilliseconds(); + const mergedMetrics = mergeWith(modifiableMetricsMap, newMetricMap, mergeMetricCustomizer); + dispatch(setMetrics(mergedMetrics)); +}; export const loadMetrics = () => async (dispatch) => { const pplService = getPPLService(); @@ -81,15 +94,15 @@ export const loadMetrics = () => async (dispatch) => { setDataSourceIcons(coloredIconsFrom([OBSERVABILITY_CUSTOM_METRIC, ...remoteDataSources])) ); - const remoteDataRequests = fetchRemoteMetrics(remoteDataSources); + const remoteDataRequests = await fetchRemoteMetrics(remoteDataSources); const metricsResultSet = await Promise.all([customDataRequest, ...remoteDataRequests]); const metricsResult = metricsResultSet.flat(); const metricsMapById = keyBy(metricsResult.flat(), 'id'); - await dispatch(mergeMetrics(metricsMapById)); + dispatch(mergeMetrics(metricsMapById)); const sortedIds = sortBy(metricsResult, 'catalog', 'id').map((m) => m.id); - await dispatch(setSortedIds(sortedIds)); + dispatch(setSortedIds(sortedIds)); }; const fetchCustomMetrics = async () => { @@ -150,17 +163,9 @@ export const metricSlice = createSlice({ name: REDUX_SLICE_METRICS, initialState, reducers: { - mergeMetrics: (state, { payload }) => { - const { metrics } = state; - const modifiableMetricsMap = { ...metrics }; - - const mergedMetrics = mergeWith(modifiableMetricsMap, payload, mergeMetricCustomizer); - state.metrics = mergedMetrics; + setMetrics: (state, { payload }) => { + state.metrics = payload; }, - - // setMetrics: (state, { payload }) => { - // state.metrics = payload; - // }, setMetric: (state, { payload }) => { state.metrics[payload.id] = payload; }, @@ -214,8 +219,7 @@ export const metricSlice = createSlice({ export const { deSelectMetric, clearSelectedMetrics, - - mergeMetrics, + selectMetric, moveMetric, setSearch, setDateSpan, @@ -227,7 +231,7 @@ export const { /** private actions */ -const { selectMetric, setMetric, setSortedIds } = metricSlice.actions; +const { setMetrics, setMetric, setSortedIds } = metricSlice.actions; const getAvailableAttributes = (id, metricIndex) => async (dispatch, getState) => { const { toasts } = coreRefs; @@ -300,16 +304,12 @@ export const availableMetricsSelector = (state) => { export const selectedMetricsSelector = (state) => pick(state.metrics.metrics, state.metrics.selectedIds) ?? {}; -export const selectedMetricByIdSelector = (id) => (state) => state.metrics.metrics[id]; - export const selectedMetricsIdsSelector = (state) => state.metrics.selectedIds ?? []; export const searchSelector = (state) => state.metrics.search; export const metricIconsSelector = (state) => state.metrics.dataSourceIcons; -export const metricsLayoutSelector = (state) => state.metrics.metricsLayout; - export const dateSpanFilterSelector = (state) => state.metrics.dateSpanFilter; export const refreshSelector = (state) => state.metrics.refresh; diff --git a/public/components/metrics/sidebar/sidebar.tsx b/public/components/metrics/sidebar/sidebar.tsx index 6afb40c1fb..ac93037fc5 100644 --- a/public/components/metrics/sidebar/sidebar.tsx +++ b/public/components/metrics/sidebar/sidebar.tsx @@ -43,10 +43,12 @@ export const Sidebar = ({ useEffect(() => { if (additionalMetric) { - dispatch(clearSelectedMetrics()); - dispatch(addSelectedMetric(additionalMetric)); + (async function () { + await dispatch(clearSelectedMetrics()); + await dispatch(addSelectedMetric(additionalMetric)); + })(); } - }, [additionalMetric]); + }, [additionalMetric?.id]); const selectedMetricsList = useMemo(() => { return selectedMetricsIds.map((id) => selectedMetrics[id]).filter((m) => m); // filter away null entries diff --git a/public/components/metrics/view/metrics_grid.tsx b/public/components/metrics/view/metrics_grid.tsx index 2c41224552..161431918d 100644 --- a/public/components/metrics/view/metrics_grid.tsx +++ b/public/components/metrics/view/metrics_grid.tsx @@ -4,7 +4,7 @@ */ import React, { useEffect, useMemo } from 'react'; -import { EuiDragDropContext, EuiDraggable, EuiDroppable } from '@elastic/eui'; +import { EuiContextMenuItem, EuiDragDropContext, EuiDraggable, EuiDroppable } from '@elastic/eui'; import { useObservable } from 'react-use'; import { connect } from 'react-redux'; import { CoreStart } from '../../../../../../src/core/public'; @@ -53,6 +53,19 @@ const visualizationFromMetric = (metric, dateSpanFilter): SavedVisualizationType }, }); +const promQLActionMenu = [ + { + closeActionsMenu(); + showModal('catalogModal'); + }} + > + View query + , +]; + const navigateToEventExplorerVisualization = (savedVisualizationId: string) => { window.location.assign(`${observabilityLogsID}#/explorer/${savedVisualizationId}`); }; @@ -80,6 +93,7 @@ export const InnerGridVisualization = ({ id, idx, dateSpanFilter, metric, refres inlineEditor={ metric.subType === PROMQL_METRIC_SUBTYPE && } + actionMenuType="metricsGrid" /> ); diff --git a/public/components/notebooks/components/__tests__/__snapshots__/notebook.test.tsx.snap b/public/components/notebooks/components/__tests__/__snapshots__/notebook.test.tsx.snap index 90933ace6d..b2bc5ba057 100644 --- a/public/components/notebooks/components/__tests__/__snapshots__/notebook.test.tsx.snap +++ b/public/components/notebooks/components/__tests__/__snapshots__/notebook.test.tsx.snap @@ -1,6 +1,344 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` spec renders the component 1`] = ` +exports[` spec Renders the empty component 1`] = ` +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+

+ sample-notebook-1 +

+
+
+
+
+
+

+ Created +
+ + 12/14/2023 06:49 PM +

+
+
+
+
+
+
+
+
+
+

+ No paragraphs +

+
+ Add a paragraph to compose your document or story. Notebooks now support two types of input: +
+
+
+
+
+
+
+
+
+ +
+
+ + Code block + +
+

+ Write contents directly using markdown, SQL or PPL. +

+
+
+ +
+
+
+
+
+ +
+
+ + Visualization + +
+

+ Import OpenSearch Dashboards or Observability visualizations to the notes. +

+
+
+ +
+
+
+
+
+
+
+
+
+
+`; + +exports[` spec Renders the visualization component 1`] = `
@@ -313,7 +651,7 @@ exports[` spec renders the component 1`] = `
`; -exports[` spec renders the empty component 1`] = ` +exports[` spec test reporting action button 1`] = `
@@ -354,14 +692,19 @@ exports[` spec renders the empty component 1`] = ` > + > + + @@ -374,7 +717,48 @@ exports[` spec renders the empty component 1`] = `
+ > +
+
+
+ +
+
+
+
@@ -394,14 +778,19 @@ exports[` spec renders the empty component 1`] = ` > + > + + @@ -418,7 +807,9 @@ exports[` spec renders the empty component 1`] = ` />

+ > + sample-notebook-1 +

@@ -436,7 +827,7 @@ exports[` spec renders the empty component 1`] = ` Created
- Invalid date + 12/14/2023 06:49 PM

@@ -487,14 +878,18 @@ exports[` spec renders the empty component 1`] = ` > + > + +
spec renders the empty component 1`] = ` > + > + +
({ describe(' spec', () => { configure({ adapter: new Adapter() }); + const props = { + loading: false, + fetchNotebooks: jest.fn(), + addSampleNotebooks: jest.fn(), + createNotebook: jest.fn(), + renameNotebook: jest.fn(), + cloneNotebook: jest.fn(), + deleteNotebook: jest.fn(), + parentBreadcrumb: { href: 'parent-href', text: 'parent-text' }, + setBreadcrumbs: jest.fn(), + setToast: jest.fn(), + }; + + const renderNoteTable = (overrides = {}) => { + const utils = render(); + // Additional setup or assertions if needed + return utils; + }; + + afterEach(() => { + cleanup(); // Cleanup the rendered component after each test + }); + it('renders the empty component', () => { - const fetchNotebooks = jest.fn(); - const addSampleNotebooks = jest.fn(); - const createNotebook = jest.fn(); - const renameNotebook = jest.fn(); - const cloneNotebook = jest.fn(); - const deleteNotebook = jest.fn(); - const setBreadcrumbs = jest.fn(); - const setToast = jest.fn(); - const utils = render( - - ); + const utils = renderNoteTable({ notebooks: [] }); expect(utils.container.firstChild).toMatchSnapshot(); }); it('renders the component', () => { - const fetchNotebooks = jest.fn(); - const addSampleNotebooks = jest.fn(); - const createNotebook = jest.fn(); - const renameNotebook = jest.fn(); - const cloneNotebook = jest.fn(); - const deleteNotebook = jest.fn(); - const setBreadcrumbs = jest.fn(); - const setToast = jest.fn(); const notebooks = Array.from({ length: 5 }, (v, k) => ({ path: `path-${k}`, id: `id-${k}`, dateCreated: '2023-01-01 12:00:00', dateModified: '2023-01-02 12:00:00', })); - const utils = render( - - ); + const utils = renderNoteTable({ notebooks }); expect(utils.container.firstChild).toMatchSnapshot(); - utils.getByText('Actions').click(); - utils.getByText('Add samples').click(); - utils.getAllByLabelText('Select this row')[0].click(); - utils.getByText('Actions').click(); - utils.getByText('Delete').click(); - utils.getByText('Cancel').click(); - utils.getAllByLabelText('Select this row')[0].click(); - utils.getByText('Actions').click(); - utils.getByText('Rename').click(); + fireEvent.click(utils.getByText('Actions')); + fireEvent.click(utils.getByText('Add samples')); + fireEvent.click(utils.getAllByLabelText('Select this row')[0]); + fireEvent.click(utils.getByText('Actions')); + fireEvent.click(utils.getByText('Delete')); + fireEvent.click(utils.getByText('Cancel')); + fireEvent.click(utils.getAllByLabelText('Select this row')[0]); + fireEvent.click(utils.getByText('Actions')); + fireEvent.click(utils.getByText('Rename')); }); - it('create notebook', async () => { - const fetchNotebooks = jest.fn(); - const addSampleNotebooks = jest.fn(); - const createNotebook = jest.fn(); - const renameNotebook = jest.fn(); - const cloneNotebook = jest.fn(); - const deleteNotebook = jest.fn(); - const setBreadcrumbs = jest.fn(); - const setToast = jest.fn(); + it('create notebook modal', async () => { const notebooks = Array.from({ length: 5 }, (v, k) => ({ path: `path-${k}`, id: `id-${k}`, dateCreated: 'date-created', dateModified: 'date-modified', })); - const utils = render( - - ); - utils.getByText('Create notebook').click(); + const utils = renderNoteTable({ notebooks }); + fireEvent.click(utils.getByText('Create notebook')); await waitFor(() => { expect(global.window.location.href).toContain('/create'); }); }); + + it('filters notebooks based on search input', () => { + const { getByPlaceholderText, getAllByText, queryByText } = renderNoteTable({ + notebooks: [ + { + path: 'path-1', + id: 'id-1', + dateCreated: 'date-created', + dateModified: 'date-modified', + }, + ], + }); + + const searchInput = getByPlaceholderText('Search notebook name'); + fireEvent.change(searchInput, { target: { value: 'path-1' } }); + + // Assert that only the matching notebook is displayed + expect(getAllByText('path-1')).toHaveLength(1); + expect(queryByText('path-0')).toBeNull(); + expect(queryByText('path-2')).toBeNull(); + }); + + it('displays empty state message and create notebook button', () => { + const { getAllByText, getAllByTestId } = renderNoteTable({ notebooks: [] }); + + expect(getAllByText('No notebooks')).toHaveLength(1); + + // Create notebook using the modal + fireEvent.click(getAllByText('Create notebook')[0]); + fireEvent.click(getAllByTestId('custom-input-modal-input')[0]); + fireEvent.input(getAllByTestId('custom-input-modal-input')[0], { + target: { value: 'test-notebook' }, + }); + fireEvent.click(getAllByText('Create')[0]); + expect(props.createNotebook).toHaveBeenCalledTimes(1); + }); + + it('renames a notebook', () => { + const notebooks = [ + { + path: 'path-1', + id: 'id-1', + dateCreated: 'date-created', + dateModified: 'date-modified', + }, + ]; + const { getByText, getByLabelText, getAllByText, getByTestId } = renderNoteTable({ notebooks }); + + // Select a notebook + fireEvent.click(getByLabelText('Select this row')); + + // Open Actions dropdown and click Rename + fireEvent.click(getByText('Actions')); + fireEvent.click(getByText('Rename')); + + // Ensure the modal is open (you may need to adjust based on your modal implementation) + expect(getAllByText('Rename notebook')).toHaveLength(1); + + // Mock user input and submit + fireEvent.input(getByTestId('custom-input-modal-input'), { + target: { value: 'test-notebook-newname' }, + }); + fireEvent.click(getByTestId('custom-input-modal-confirm-button')); + + // Assert that the renameNotebook function is called + expect(props.renameNotebook).toHaveBeenCalledTimes(1); + expect(props.renameNotebook).toHaveBeenCalledWith('test-notebook-newname', 'id-1'); + }); + + it('clones a notebook', () => { + const notebooks = [ + { + path: 'path-1', + id: 'id-1', + dateCreated: 'date-created', + dateModified: 'date-modified', + }, + ]; + const { getByText, getByLabelText, getAllByText, getByTestId } = renderNoteTable({ notebooks }); + + // Select a notebook + fireEvent.click(getByLabelText('Select this row')); + + // Open Actions dropdown and click Duplicate + fireEvent.click(getByText('Actions')); + fireEvent.click(getByText('Duplicate')); + + // Ensure the modal is open (you may need to adjust based on your modal implementation) + expect(getAllByText('Duplicate notebook')).toHaveLength(1); + + // Mock user input and submit + fireEvent.input(getByTestId('custom-input-modal-input'), { + target: { value: 'new-copy' }, + }); + fireEvent.click(getByTestId('custom-input-modal-confirm-button')); + + // Assert that the cloneNotebook function is called + expect(props.cloneNotebook).toHaveBeenCalledTimes(1); + expect(props.cloneNotebook).toHaveBeenCalledWith('new-copy', 'id-1'); + }); + + it('deletes a notebook', () => { + const notebooks = [ + { + path: 'path-1', + id: 'id-1', + dateCreated: 'date-created', + dateModified: 'date-modified', + }, + ]; + const { getByText, getByLabelText, getAllByText, getByTestId } = renderNoteTable({ notebooks }); + + // Select a notebook + fireEvent.click(getByLabelText('Select this row')); + + // Open Actions dropdown and click Delete + fireEvent.click(getByText('Actions')); + fireEvent.click(getByText('Delete')); + + // Ensure the modal is open (you may need to adjust based on your modal implementation) + expect(getAllByText('Delete 1 notebook')).toHaveLength(1); + + // Mock user confirmation and submit + fireEvent.input(getByTestId('delete-notebook-modal-input'), { + target: { value: 'delete' }, + }); + fireEvent.click(getByTestId('delete-notebook-modal-delete-button')); + + // Assert that the deleteNotebook function is called + expect(props.deleteNotebook).toHaveBeenCalledTimes(1); + expect(props.deleteNotebook).toHaveBeenCalledWith(['id-1'], expect.any(String)); + }); + + it('adds sample notebooks', async () => { + const { getByText, getAllByText, getByTestId } = renderNoteTable({ notebooks: [] }); + + // Open Actions dropdown and click Add samples + fireEvent.click(getByText('Actions')); + fireEvent.click(getAllByText('Add samples')[0]); + + // Ensure the modal is open (you may need to adjust based on your modal implementation) + expect(getAllByText('Add sample notebooks')).toHaveLength(1); + + // Mock user confirmation and submit + fireEvent.click(getByTestId('confirmModalConfirmButton')); + + // Assert that the addSampleNotebooks function is called + expect(props.addSampleNotebooks).toHaveBeenCalledTimes(1); + }); + + it('closes the action panel', async () => { + const { getByText, queryByTestId } = renderNoteTable({ notebooks: [] }); + expect(queryByTestId('rename-notebook-btn')).not.toBeInTheDocument(); + + // Open Actions dropdown + fireEvent.click(getByText('Actions')); + + // Ensure the action panel is open + expect(queryByTestId('rename-notebook-btn')).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(getByText('Actions')); + }); + + // Ensure the action panel is closed + expect(queryByTestId('rename-notebook-btn')).not.toBeInTheDocument(); + }); + + it('closes the delete modal', () => { + const notebooks = [ + { + path: 'path-1', + id: 'id-1', + dateCreated: 'date-created', + dateModified: 'date-modified', + }, + ]; + const { getByText, getByLabelText, queryByText } = renderNoteTable({ notebooks }); + + // Select a notebook + fireEvent.click(getByLabelText('Select this row')); + + // Open Actions dropdown and click Delete + fireEvent.click(getByText('Actions')); + fireEvent.click(getByText('Delete')); + + // Ensure the modal is open + expect(getByText('Delete 1 notebook')).toBeInTheDocument(); + + // Close the delete modal + fireEvent.click(getByText('Cancel')); + + // Ensure the delete modal is closed + expect(queryByText('Delete 1 notebook')).toBeNull(); + }); }); diff --git a/public/components/notebooks/components/__tests__/notebook.test.tsx b/public/components/notebooks/components/__tests__/notebook.test.tsx index f31508852e..eb33888af0 100644 --- a/public/components/notebooks/components/__tests__/notebook.test.tsx +++ b/public/components/notebooks/components/__tests__/notebook.test.tsx @@ -3,17 +3,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { fireEvent, render, waitFor } from '@testing-library/react'; -import { configure, mount, shallow } from 'enzyme'; +import '@testing-library/jest-dom'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; -import PPLService from '../../../../services/requests/ppl'; import React from 'react'; import { HttpResponse } from '../../../../../../../src/core/public'; -import httpClientMock from '../../../../../test/__mocks__/httpClientMock'; -import { sampleNotebook1 } from '../helpers/__tests__/sampleDefaultNotebooks'; -import { Notebook } from '../notebook'; -import { SavedObjectsActions } from '../../../../services/saved_objects/saved_object_client/saved_objects_actions'; +import { getOSDHttp } from '../../../../../common/utils'; +import { + addCodeBlockResponse, + clearOutputNotebook, + codeBlockNotebook, + codePlaceholderText, + emptyNotebook, + notebookPutResponse, + runCodeBlockResponse, + sampleNotebook1, +} from '../../../../../test/notebooks_constants'; import { sampleSavedVisualization } from '../../../../../test/panels_constants'; +import PPLService from '../../../../services/requests/ppl'; +import { SavedObjectsActions } from '../../../../services/saved_objects/saved_object_client/saved_objects_actions'; +import { Notebook } from '../notebook'; jest.mock('../../../../../../../src/plugins/embeddable/public', () => ({ ViewMode: { @@ -36,23 +46,27 @@ global.fetch = jest.fn(() => describe(' spec', () => { configure({ adapter: new Adapter() }); + const httpClient = getOSDHttp(); + const pplService = new PPLService(httpClient); + const setBreadcrumbs = jest.fn(); + const renameNotebook = jest.fn(); + const cloneNotebook = jest.fn(); + const deleteNotebook = jest.fn(); + const setToast = jest.fn(); + const location = jest.fn() as any; + location.search = ''; + const history = jest.fn() as any; + history.replace = jest.fn(); + history.push = jest.fn(); - it('renders the empty component', async () => { - const pplService = new PPLService(httpClientMock); - const setBreadcrumbs = jest.fn(); - const renameNotebook = jest.fn(); - const cloneNotebook = jest.fn(); - const deleteNotebook = jest.fn(); - const setToast = jest.fn(); - const location = jest.fn(); - const history = jest.fn() as any; - history.replace = jest.fn(); + it('Renders the empty component', async () => { + httpClient.get = jest.fn(() => Promise.resolve((emptyNotebook as unknown) as HttpResponse)); const utils = render( spec', () => { history={history} /> ); + await waitFor(() => { + expect(utils.getByText('sample-notebook-1')).toBeInTheDocument(); + }); expect(utils.container.firstChild).toMatchSnapshot(); - utils.getByText('Add code block').click(); - utils.getByText('Add visualization').click(); }); - it('renders the component', async () => { - const pplService = new PPLService(httpClientMock); - const setBreadcrumbs = jest.fn(); - const renameNotebook = jest.fn(); - const cloneNotebook = jest.fn(); - const deleteNotebook = jest.fn(); - const setToast = jest.fn(); - const location = jest.fn(); - const history = jest.fn() as any; - history.replace = jest.fn(); + it('test reporting action button', async () => { + httpClient.get = jest.fn(() => Promise.resolve((emptyNotebook as unknown) as HttpResponse)); + const utils = render( + + ); + await waitFor(() => { + expect(utils.getByText('sample-notebook-1')).toBeInTheDocument(); + }); + expect(utils.container.firstChild).toMatchSnapshot(); + + act(() => { + fireEvent.click(utils.getByText('Reporting actions')); + }); + + expect(utils.queryByTestId('download-notebook-pdf')).toBeInTheDocument(); + + act(() => { + fireEvent.click(utils.getByText('Reporting actions')); + }); + + await waitFor(() => { + expect(utils.queryByTestId('download-notebook-pdf')).toBeNull(); + }); + }); + + it('Adds a code block', async () => { + httpClient.get = jest.fn(() => Promise.resolve((emptyNotebook as unknown) as HttpResponse)); + let postFlag = 1; + httpClient.post = jest.fn(() => { + if (postFlag === 1) { + postFlag += 1; + return Promise.resolve((addCodeBlockResponse as unknown) as HttpResponse); + } else return Promise.resolve((runCodeBlockResponse as unknown) as HttpResponse); + }); + const utils = render( + + ); + await waitFor(() => { + expect(utils.getByText('sample-notebook-1')).toBeInTheDocument(); + }); + + act(() => { + utils.getByText('Add code block').click(); + }); + + await waitFor(() => { + expect(utils.getByPlaceholderText(codePlaceholderText)).toBeInTheDocument(); + }); + }); + + it('toggles show input in code block', async () => { + httpClient.get = jest.fn(() => Promise.resolve((emptyNotebook as unknown) as HttpResponse)); + let postFlag = 1; + httpClient.post = jest.fn(() => { + if (postFlag === 1) { + postFlag += 1; + return Promise.resolve((addCodeBlockResponse as unknown) as HttpResponse); + } else return Promise.resolve((runCodeBlockResponse as unknown) as HttpResponse); + }); + const utils = render( + + ); + await waitFor(() => { + expect(utils.getByText('sample-notebook-1')).toBeInTheDocument(); + }); + + act(() => { + utils.getByText('Add code block').click(); + }); + + await waitFor(() => { + expect(utils.getByPlaceholderText(codePlaceholderText)).toBeInTheDocument(); + }); + + act(() => { + utils.getByLabelText('Toggle show input').click(); + }); + + await waitFor(() => { + expect(utils.queryByPlaceholderText(codePlaceholderText)).toBeNull(); + }); + }); + + it('runs a code block and checks the output', async () => { + httpClient.get = jest.fn(() => Promise.resolve((emptyNotebook as unknown) as HttpResponse)); + let postFlag = 1; + httpClient.post = jest.fn(() => { + if (postFlag === 1) { + postFlag += 1; + return Promise.resolve((addCodeBlockResponse as unknown) as HttpResponse); + } else return Promise.resolve((runCodeBlockResponse as unknown) as HttpResponse); + }); + const utils = render( + + ); + await waitFor(() => { + expect(utils.getByText('sample-notebook-1')).toBeInTheDocument(); + }); + + act(() => { + utils.getByText('Add code block').click(); + }); + + await waitFor(() => { + expect(utils.getByPlaceholderText(codePlaceholderText)).toBeInTheDocument(); + }); + + act(() => { + fireEvent.input(utils.getByPlaceholderText(codePlaceholderText), { + target: { value: '%md \\n hello' }, + }); + fireEvent.click(utils.getByText('Run')); + }); + + await waitFor(() => { + expect(utils.queryByText('Run')).toBeNull(); + expect(utils.getByText('hello')).toBeInTheDocument(); + }); + }); + + it('toggles between input/output only views', async () => { + httpClient.get = jest.fn(() => Promise.resolve((emptyNotebook as unknown) as HttpResponse)); + const utils = render( + + ); + await waitFor(() => { + expect(utils.getByText('sample-notebook-1')).toBeInTheDocument(); + }); + + act(() => { + utils.getByText('Add code block').click(); + }); + + await waitFor(() => { + expect(utils.getByPlaceholderText(codePlaceholderText)).toBeInTheDocument(); + }); + + act(() => { + utils.getByLabelText('Toggle show input').click(); + }); + + await waitFor(() => { + expect(utils.queryByPlaceholderText(codePlaceholderText)).toBeNull(); + }); + + act(() => { + utils.getByLabelText('Toggle show input').click(); + }); + + act(() => { + fireEvent.click(utils.getByTestId('input_only')); + }); + + await waitFor(() => { + expect(utils.queryByText('Refresh')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(utils.getByTestId('output_only')); + }); + + await waitFor(() => { + expect(utils.queryByText('Refresh')).toBeNull(); + expect(utils.getByText('hello')).toBeInTheDocument(); + }); + }); + + it('Renders a notebook and checks paragraph actions', async () => { + httpClient.get = jest.fn(() => Promise.resolve((codeBlockNotebook as unknown) as HttpResponse)); + httpClient.put = jest.fn(() => + Promise.resolve((clearOutputNotebook as unknown) as HttpResponse) + ); + httpClient.delete = jest.fn(() => + Promise.resolve(({ paragraphs: [] } as unknown) as HttpResponse) + ); + + const utils = render( + + ); + await waitFor(() => { + expect(utils.getByText('sample-notebook-1')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(utils.getByText('Paragraph actions')); + }); + + act(() => { + fireEvent.click(utils.getByText('Clear all outputs')); + }); + + await waitFor(() => { + expect( + utils.queryByText( + 'Are you sure you want to clear all outputs? The action cannot be undone.' + ) + ).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(utils.getByTestId('confirmModalConfirmButton')); + }); + + await waitFor(() => { + expect(utils.queryByText('hello')).toBeNull(); + }); + + act(() => { + fireEvent.click(utils.getByText('Paragraph actions')); + }); + + act(() => { + fireEvent.click(utils.getByText('Delete all paragraphs')); + }); + + await waitFor(() => { + expect( + utils.queryByText( + 'Are you sure you want to delete all paragraphs? The action cannot be undone.' + ) + ).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(utils.getByTestId('confirmModalConfirmButton')); + }); + + await waitFor(() => { + expect(utils.queryByText('No paragraphs')).toBeInTheDocument(); + }); + }); + + it('Checks notebook rename action', async () => { + const renameNotebookMock = jest.fn(() => + Promise.resolve((notebookPutResponse as unknown) as HttpResponse) + ); + const cloneNotebookMock = jest.fn(() => Promise.resolve('dummy-string')); + httpClient.get = jest.fn(() => Promise.resolve((codeBlockNotebook as unknown) as HttpResponse)); + + httpClient.put = jest.fn(() => { + return Promise.resolve((notebookPutResponse as unknown) as HttpResponse); + }); + + httpClient.post = jest.fn(() => { + return Promise.resolve((addCodeBlockResponse as unknown) as HttpResponse); + }); + + const utils = render( + + ); + await waitFor(() => { + expect(utils.getByText('sample-notebook-1')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(utils.getByText('Notebook actions')); + }); + + act(() => { + fireEvent.click(utils.getByText('Rename notebook')); + }); + + await waitFor(() => { + expect(utils.queryByTestId('custom-input-modal-input')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.input(utils.getByTestId('custom-input-modal-input'), { + target: { value: 'test-notebook-newname' }, + }); + fireEvent.click(utils.getByTestId('custom-input-modal-confirm-button')); + }); + + await waitFor(() => { + expect(renameNotebookMock).toHaveBeenCalledTimes(1); + }); + }); + + it('Checks notebook clone action', async () => { + const renameNotebookMock = jest.fn(() => + Promise.resolve((notebookPutResponse as unknown) as HttpResponse) + ); + const cloneNotebookMock = jest.fn(() => Promise.resolve('dummy-string')); + httpClient.get = jest.fn(() => Promise.resolve((codeBlockNotebook as unknown) as HttpResponse)); + + httpClient.put = jest.fn(() => { + return Promise.resolve((notebookPutResponse as unknown) as HttpResponse); + }); + + httpClient.post = jest.fn(() => { + return Promise.resolve((addCodeBlockResponse as unknown) as HttpResponse); + }); + + const utils = render( + + ); + await waitFor(() => { + expect(utils.getByText('sample-notebook-1')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(utils.getByText('Notebook actions')); + }); + + act(() => { + fireEvent.click(utils.getByText('Duplicate notebook')); + }); + + await waitFor(() => { + expect(utils.queryByTestId('custom-input-modal-input')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(utils.getByTestId('custom-input-modal-confirm-button')); + }); + + expect(cloneNotebookMock).toHaveBeenCalledTimes(1); + }); + + it('Checks notebook delete action', async () => { + const renameNotebookMock = jest.fn(() => + Promise.resolve((notebookPutResponse as unknown) as HttpResponse) + ); + const cloneNotebookMock = jest.fn(() => Promise.resolve('dummy-string')); + httpClient.get = jest.fn(() => Promise.resolve((codeBlockNotebook as unknown) as HttpResponse)); + + httpClient.put = jest.fn(() => { + return Promise.resolve((notebookPutResponse as unknown) as HttpResponse); + }); + + httpClient.post = jest.fn(() => { + return Promise.resolve((addCodeBlockResponse as unknown) as HttpResponse); + }); + + const utils = render( + + ); + await waitFor(() => { + expect(utils.getByText('sample-notebook-1')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(utils.getByText('Notebook actions')); + }); + + act(() => { + fireEvent.click(utils.getByText('Delete notebook')); + }); + + await waitFor(() => { + expect(utils.queryByTestId('delete-notebook-modal-input')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.input(utils.getByTestId('delete-notebook-modal-input'), { + target: { value: 'delete' }, + }); + }); + + act(() => { + fireEvent.click(utils.getByTestId('delete-notebook-modal-delete-button')); + }); + + expect(deleteNotebook).toHaveBeenCalledTimes(1); + }); + it('Renders the visualization component', async () => { SavedObjectsActions.getBulk = jest.fn().mockResolvedValue({ observabilityObjectList: [{ savedVisualization: sampleSavedVisualization }], }); - httpClientMock.get = jest.fn(() => + httpClient.get = jest.fn(() => Promise.resolve(({ ...sampleNotebook1, path: sampleNotebook1.name, @@ -108,7 +596,7 @@ describe(' spec', () => { pplService={pplService} openedNoteId={sampleNotebook1.id} DashboardContainerByValueRenderer={jest.fn()} - http={httpClientMock} + http={httpClient} parentBreadcrumb={{ href: 'parent-href', text: 'parent-text' }} setBreadcrumbs={setBreadcrumbs} renameNotebook={renameNotebook} diff --git a/public/components/notebooks/components/helpers/__tests__/default_parser.test.tsx b/public/components/notebooks/components/helpers/__tests__/default_parser.test.tsx index 3c03a3d5b9..2339b95026 100644 --- a/public/components/notebooks/components/helpers/__tests__/default_parser.test.tsx +++ b/public/components/notebooks/components/helpers/__tests__/default_parser.test.tsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { defaultParagraphParser } from '../default_parser'; import { sampleNotebook1, sampleNotebook2, @@ -12,18 +11,18 @@ import { sampleNotebook5, sampleParsedParagraghs1, sampleParsedParagraghs2, -} from './sampleDefaultNotebooks'; +} from '../../../../../../test/notebooks_constants'; +import { defaultParagraphParser } from '../default_parser'; // Perfect schema describe('Testing default backend parser function with perfect schema', () => { - test('defaultParagraphParserTest1', (done) => { + it('defaultParagraphParserTest1', () => { const parsedParagraphs1 = defaultParagraphParser(sampleNotebook1.paragraphs); const parsedParagraphs2 = defaultParagraphParser(sampleNotebook2.paragraphs); const parsedParagraphs3 = defaultParagraphParser([]); expect(parsedParagraphs1).toEqual(sampleParsedParagraghs1); expect(parsedParagraphs2).toEqual(sampleParsedParagraghs2); expect(parsedParagraphs3).toEqual([]); - done(); }); it('returns parsed paragraphs', () => { @@ -82,16 +81,15 @@ describe('Testing default backend parser function with perfect schema', () => { // Issue in schema describe('Testing default backend parser function with wrong schema', () => { - test('defaultParagraphParserTest2', (done) => { + it('defaultParagraphParserTest2', () => { expect(() => { - const parsedParagraphs1 = defaultParagraphParser(sampleNotebook3.paragraphs); + const _parsedParagraphs1 = defaultParagraphParser(sampleNotebook3.paragraphs); }).toThrow(Error); expect(() => { - const parsedParagraphs2 = defaultParagraphParser(sampleNotebook4.paragraphs); + const _parsedParagraphs2 = defaultParagraphParser(sampleNotebook4.paragraphs); }).toThrow(Error); expect(() => { - const parsedParagraphs3 = defaultParagraphParser(sampleNotebook5.paragraphs); + const _parsedParagraphs3 = defaultParagraphParser(sampleNotebook5.paragraphs); }).toThrow(Error); - done(); }); }); diff --git a/public/components/notebooks/components/helpers/__tests__/sampleZeppelinNotebooks.tsx b/public/components/notebooks/components/helpers/__tests__/sampleZeppelinNotebooks.tsx deleted file mode 100644 index db07da1c51..0000000000 --- a/public/components/notebooks/components/helpers/__tests__/sampleZeppelinNotebooks.tsx +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// Sample notebook with all input and output -export const sampleNotebook1 = { - paragraphs: [ - { - text: - "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", - user: 'anonymous', - dateUpdated: '2020-08-20 21:15:04.590', - config: {}, - settings: { params: {}, forms: {} }, - results: { - code: 'SUCCESS', - msg: [ - { - type: 'HTML', - data: - '
\n

Hi Everyone

\n
    \n
  • Here’s a demo on OpenSearch Dashboards Notebooks
  • \n
  • You may use the top left buttons to play around with notebooks and Paragraphs
  • \n
\n\n
', - }, - ], - }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958104590_901298942', - id: 'paragraph_1596519508360_932236116', - dateCreated: '2020-08-20 21:15:04.590', - status: 'READY', - }, - { - title: 'VISUALIZATION', - text: - '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - user: 'anonymous', - dateUpdated: '2020-08-20 21:25:28.588', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958728587_1310320520', - id: 'paragraph_1597958728587_1310320520', - dateCreated: '2020-08-20 21:25:28.587', - status: 'READY', - }, - ], - name: 'Embed Vizualization', - id: '2FJH8PW8K', - defaultInterpreterGroup: 'spark', - version: '0.9.0-preview2', - noteParams: {}, - noteForms: {}, - angularObjects: {}, - config: { isZeppelinNotebookCronEnable: false }, - info: {}, -}; - -// Parsed Output of sample notebook1 -export const sampleParsedParagraghs1 = [ - { - uniqueId: 'paragraph_1596519508360_932236116', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: false, - vizObjectInput: '', - id: 1, - inp: - "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", - lang: 'text/x-', - editorLanguage: '', - typeOut: ['HTML'], - out: [ - '
\n

Hi Everyone

\n
    \n
  • Here’s a demo on OpenSearch Dashboards Notebooks
  • \n
  • You may use the top left buttons to play around with notebooks and Paragraphs
  • \n
\n\n
', - ], - }, - { - uniqueId: 'paragraph_1597958728587_1310320520', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: true, - vizObjectInput: - '{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - id: 2, - inp: - '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - lang: 'text/x-', - editorLanguage: '', - typeOut: [], - out: [], - }, -]; - -// Sample notebook with all input and cleared outputs -export const sampleNotebook2 = { - paragraphs: [ - { - text: - "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", - user: 'anonymous', - dateUpdated: '2020-08-20 21:15:04.590', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958104590_901298942', - id: 'paragraph_1596519508360_932236116', - dateCreated: '2020-08-20 21:15:04.590', - status: 'READY', - }, - { - title: 'Paragraph inserted', - text: '%md\n\n## Greetings!\n* Yay! you may import and export me ', - user: 'anonymous', - dateUpdated: '2020-08-20 21:15:04.590', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958104590_1715920734', - id: 'paragraph_1596742076640_674206137', - dateCreated: '2020-08-20 21:15:04.590', - status: 'READY', - }, - { - title: 'Paragraph inserted', - text: - "%md\n\n### Let's use Visualization API with dashboard container to embed Visualizations in notebooks\n2. **Unpin** the container to *edit the size* or *delete it*\n3. **Refresh** the container after *date is changed*", - user: 'anonymous', - dateUpdated: '2020-08-20 21:15:04.590', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958104590_931410594', - id: 'paragraph_1596524302932_2112910756', - dateCreated: '2020-08-20 21:15:04.590', - status: 'READY', - }, - { - title: 'VISUALIZATION', - text: - '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - user: 'anonymous', - dateUpdated: '2020-08-20 21:25:28.588', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958728587_1310320520', - id: 'paragraph_1597958728587_1310320520', - dateCreated: '2020-08-20 21:25:28.587', - status: 'READY', - }, - ], - name: 'Embed Vizualization', - id: '2FJH8PW8K', - defaultInterpreterGroup: 'spark', - version: '0.9.0-preview2', - noteParams: {}, - noteForms: {}, - angularObjects: {}, - config: { isZeppelinNotebookCronEnable: false }, - info: {}, -}; - -// Parsed Output of sample notebook2 -export const sampleParsedParagraghs2 = [ - { - uniqueId: 'paragraph_1596519508360_932236116', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: false, - vizObjectInput: '', - id: 1, - inp: - "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", - lang: 'text/x-', - editorLanguage: '', - typeOut: [], - out: [], - }, - { - uniqueId: 'paragraph_1596742076640_674206137', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: false, - vizObjectInput: '', - id: 2, - inp: '%md\n\n## Greetings!\n* Yay! you may import and export me ', - lang: 'text/x-md', - editorLanguage: 'md', - typeOut: [], - out: [], - }, - { - uniqueId: 'paragraph_1596524302932_2112910756', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: false, - vizObjectInput: '', - id: 3, - inp: - "%md\n\n### Let's use Visualization API with dashboard container to embed Visualizations in notebooks\n2. **Unpin** the container to *edit the size* or *delete it*\n3. **Refresh** the container after *date is changed*", - lang: 'text/x-md', - editorLanguage: 'md', - typeOut: [], - out: [], - }, - { - uniqueId: 'paragraph_1597958728587_1310320520', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: true, - vizObjectInput: - '{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - id: 4, - inp: - '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - lang: 'text/x-', - editorLanguage: '', - typeOut: [], - out: [], - }, -]; - -// Sample notebook with no paragraph Id -export const sampleNotebook3 = { - paragraphs: [ - { - text: - "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", - user: 'anonymous', - dateUpdated: '2020-08-20 21:15:04.590', - config: {}, - settings: { params: {}, forms: {} }, - results: { - code: 'SUCCESS', - msg: [ - { - type: 'HTML', - data: - '
\n

Hi Everyone

\n
    \n
  • Here’s a demo on OpenSearch Dashboards Notebooks
  • \n
  • You may use the top left buttons to play around with notebooks and Paragraphs
  • \n
\n\n
', - }, - ], - }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958104590_901298942', - dateCreated: '2020-08-20 21:15:04.590', - status: 'READY', - }, - ], - name: 'Embed Vizualization', - id: '2FJH8PW8K', - defaultInterpreterGroup: 'spark', - version: '0.9.0-preview2', - noteParams: {}, - noteForms: {}, - angularObjects: {}, - config: { isZeppelinNotebookCronEnable: false }, - info: {}, -}; - -// Sample notebook with no VISUALIZAITON title -export const sampleNotebook4 = { - paragraphs: [ - { - text: - '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - user: 'anonymous', - dateUpdated: '2020-08-20 21:25:28.588', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958728587_1310320520', - id: 'paragraph_1597958728587_1310320520', - dateCreated: '2020-08-20 21:25:28.587', - status: 'READY', - }, - ], - name: 'Embed Vizualization', - id: '2FJH8PW8K', - defaultInterpreterGroup: 'spark', - version: '0.9.0-preview2', - noteParams: {}, - noteForms: {}, - angularObjects: {}, - config: { isZeppelinNotebookCronEnable: false }, - info: {}, -}; - -// Sample notebook with no input and output -export const sampleNotebook5 = { - paragraphs: [ - { - user: 'anonymous', - dateUpdated: '2020-08-20 21:25:28.588', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958728587_1310320520', - id: 'paragraph_1597958728587_1310320520', - dateCreated: '2020-08-20 21:25:28.587', - status: 'READY', - }, - ], - name: 'Embed Vizualization', - id: '2FJH8PW8K', - defaultInterpreterGroup: 'spark', - version: '0.9.0-preview2', - noteParams: {}, - noteForms: {}, - angularObjects: {}, - config: { isZeppelinNotebookCronEnable: false }, - info: {}, -}; diff --git a/public/components/notebooks/components/helpers/__tests__/zeppelin_parser.test.tsx b/public/components/notebooks/components/helpers/__tests__/zeppelin_parser.test.tsx deleted file mode 100644 index e6c5b351e1..0000000000 --- a/public/components/notebooks/components/helpers/__tests__/zeppelin_parser.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { zeppelinParagraphParser } from '../zeppelin_parser'; -import { - sampleNotebook1, - sampleNotebook2, - sampleNotebook3, - sampleNotebook4, - sampleNotebook5, - sampleParsedParagraghs1, - sampleParsedParagraghs2, -} from './sampleZeppelinNotebooks'; - -// Perfect schema -describe('Testing Zeppelin backend parser function with perfect schema', () => { - test('zeppelinParagraphParserTest1', (done) => { - const parsedParagraphs1 = zeppelinParagraphParser(sampleNotebook1.paragraphs); - const parsedParagraphs2 = zeppelinParagraphParser(sampleNotebook2.paragraphs); - const parsedParagraphs3 = zeppelinParagraphParser([]); - expect(parsedParagraphs1).toEqual(sampleParsedParagraghs1); - expect(parsedParagraphs2).toEqual(sampleParsedParagraghs2); - expect(parsedParagraphs3).toEqual([]); - done(); - }); -}); - -// Issue in schema -describe('Testing default backend parser function with wrong schema', () => { - test('zeppelinParagraphParserTest2', (done) => { - expect(() => { - const parsedParagraphs1 = zeppelinParagraphParser(sampleNotebook3.paragraphs); - }).toThrow(Error); - expect(() => { - const parsedParagraphs2 = zeppelinParagraphParser(sampleNotebook4.paragraphs); - }).toThrow(Error); - expect(() => { - const parsedParagraphs3 = zeppelinParagraphParser(sampleNotebook5.paragraphs); - }).toThrow(Error); - done(); - }); -}); diff --git a/public/components/notebooks/components/helpers/zeppelin_parser.tsx b/public/components/notebooks/components/helpers/zeppelin_parser.tsx deleted file mode 100644 index f2293c3a72..0000000000 --- a/public/components/notebooks/components/helpers/zeppelin_parser.tsx +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -/* This file contains parsing functions - * These functions have to be changed based on backend configuration - * If backend changes the incoming paragraph structures may change, so parsing adapts to it - */ - -import { ParaType } from '../../../common'; - -const visualizationPrefix = '%sh #vizobject:'; -const observabilityVisualizationPrefix = '%sh #observabilityviz:'; - -const langSupport = { - '%sh': 'shell', - '%md': 'md', - '%python': 'python', - '%opensearchsql': 'sql', - '%elasticsearch': 'json', -}; - -// Get the coding language from a Zeppelin paragraph input -// Param: textHeader-> header on a Zeppelin paragraph example "%md" -const parseCodeLanguage = (textHeader: string) => { - const codeLanguage = langSupport[textHeader]; - return codeLanguage || ''; -}; - -// Get the type of output message from a Zeppelin paragraph -// Param: Zeppelin Paragraph -const parseMessage = (paraObject: any) => { - try { - let mtype = []; - let mdata = []; - paraObject.results.msg.map((msg: { type: string; data: string }) => { - mtype.push(msg.type); - mdata.push(msg.data); - }); - return { - outputType: mtype, - outputData: mdata, - }; - } catch (error) { - return { - outputType: [], - outputData: [], - }; - } -}; - -// Get the type of output message from a Zeppelin paragraph -// Param: Zeppelin Paragraph -const parseText = (paraObject: any) => { - if ('text' in paraObject) { - return paraObject.text; - } else { - throw new Error('Input text parse issue'); - } -}; - -// Get the visualization from a Zeppelin Paragraph input -// All Visualizations in Zeppelin are stored as shell comment -> "%sh #vizobject:" -// TODO: This is a workaround need to look for better solutions -// Param: Zeppelin Paragraph -const parseVisualization = (paraObject: any) => { - let vizContent = ''; - if ( - paraObject.hasOwnProperty('text') && - paraObject.text.substring(0, 15) === visualizationPrefix - ) { - if (paraObject.title !== 'VISUALIZATION') { - throw new Error('Visualization parse issue'); - } - vizContent = paraObject.text.substring(15); - return { - isViz: true, - VizObject: vizContent, - }; - } - - if ( - paraObject.hasOwnProperty('text') && - paraObject.text.substring(0, 22) === observabilityVisualizationPrefix - ) { - if (paraObject.title !== 'OBSERVABILITY_VISUALIZATION') { - throw new Error('Visualization parse issue'); - } - vizContent = paraObject.text.substring(22); - return { - isViz: true, - VizObject: vizContent, - }; - } - - return { - isViz: false, - VizObject: vizContent, - }; -}; - -// This parser is used to get paragraph id -// Param: Zeppelin Paragraph -const parseId = (paraObject: any) => { - if ('id' in paraObject) { - return paraObject.id; - } else { - throw new Error('Id not found in paragraph'); - } -}; - -// This parser helps to convert Zeppelin paragraphs to a common ParaType format -// This parsing makes any backend notebook compatible with notebooks plugin -export const zeppelinParagraphParser = (zeppelinBackendParagraphs: any) => { - let parsedPara: Array = []; - try { - zeppelinBackendParagraphs.map((paraObject: ParaType, index: number) => { - const paragraphId = parseId(paraObject); - const vizParams = parseVisualization(paraObject); - const inputParam = parseText(paraObject); - const codeLanguage = parseCodeLanguage(inputParam.split('\n')[0].split('.')[0]); - const message = parseMessage(paraObject); - - let tempPara = { - uniqueId: paragraphId, - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: vizParams.isViz, - vizObjectInput: vizParams.VizObject, - id: index + 1, - inp: inputParam, - lang: 'text/x-' + codeLanguage, - editorLanguage: codeLanguage, - typeOut: message.outputType, - out: message.outputData, - }; - parsedPara.push(tempPara); - }); - return parsedPara; - } catch (error) { - throw new Error('Parsing Paragraph Issue ' + error); - } -}; diff --git a/public/components/notebooks/components/note_table.tsx b/public/components/notebooks/components/note_table.tsx index 9c63e00082..a25a1d7a68 100644 --- a/public/components/notebooks/components/note_table.tsx +++ b/public/components/notebooks/components/note_table.tsx @@ -36,14 +36,13 @@ import { CREATE_NOTE_MESSAGE, NOTEBOOKS_DOCUMENTATION_URL, } from '../../../../common/constants/notebooks'; -import { UI_DATE_FORMAT } from '../../../../common/constants/shared'; +import { UI_DATE_FORMAT, pageStyles } from '../../../../common/constants/shared'; import { DeleteNotebookModal, getCustomModal, getSampleNotebooksModal, } from './helpers/modal_containers'; import { NotebookType } from './main'; -import { pageStyles } from '../../../../common/constants/shared'; interface NoteTableProps { loading: boolean; @@ -222,6 +221,7 @@ export function NoteTable({ setIsActionsPopoverOpen(false); renameNote(); }} + data-test-subj="rename-notebook-btn" > Rename , @@ -232,6 +232,7 @@ export function NoteTable({ setIsActionsPopoverOpen(false); cloneNote(); }} + data-test-subj="duplicate-notebook-btn" > Duplicate , @@ -242,6 +243,7 @@ export function NoteTable({ setIsActionsPopoverOpen(false); deleteNote(); }} + data-test-subj="delete-notebook-btn" > Delete , @@ -251,6 +253,7 @@ export function NoteTable({ setIsActionsPopoverOpen(false); addSampleNotebooksModal(); }} + data-test-subj="add-samples-btn" > Add samples , diff --git a/public/components/notebooks/components/notebook.tsx b/public/components/notebooks/components/notebook.tsx index d86d8486fe..9db0114126 100644 --- a/public/components/notebooks/components/notebook.tsx +++ b/public/components/notebooks/components/notebook.tsx @@ -6,7 +6,7 @@ import { EuiButton, EuiButtonGroup, - EuiButtonGroupOption, + EuiButtonGroupOptionProps, EuiCard, EuiContextMenu, EuiContextMenuPanelDescriptor, @@ -27,16 +27,15 @@ import moment from 'moment'; import queryString from 'query-string'; import React, { Component } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import PPLService from '../../../services/requests/ppl'; import { ChromeBreadcrumb, CoreStart } from '../../../../../../src/core/public'; import { DashboardStart } from '../../../../../../src/plugins/dashboard/public'; import { CREATE_NOTE_MESSAGE, NOTEBOOKS_API_PREFIX, - NOTEBOOKS_SELECTED_BACKEND, } from '../../../../common/constants/notebooks'; import { UI_DATE_FORMAT } from '../../../../common/constants/shared'; import { ParaType } from '../../../../common/types/notebooks'; +import PPLService from '../../../services/requests/ppl'; import { GenerateReportLoadingModal } from './helpers/custom_modals/reporting_loading_modal'; import { defaultParagraphParser } from './helpers/default_parser'; import { DeleteNotebookModal, getCustomModal, getDeleteModal } from './helpers/modal_containers'; @@ -45,7 +44,6 @@ import { contextMenuViewReports, generateInContextReport, } from './helpers/reporting_context_menu_helper'; -import { zeppelinParagraphParser } from './helpers/zeppelin_parser'; import { Paragraphs } from './paragraph_components/paragraphs'; const panelStyles: CSS.Properties = { float: 'left', @@ -140,12 +138,7 @@ export class Notebook extends Component { try { let parsedPara; // @ts-ignore - if (NOTEBOOKS_SELECTED_BACKEND === 'ZEPPELIN') { - parsedPara = zeppelinParagraphParser(paragraphs); - this.setState({ vizPrefix: '%sh #vizobject:' }); - } else { - parsedPara = defaultParagraphParser(paragraphs); - } + parsedPara = defaultParagraphParser(paragraphs); parsedPara.forEach((para: ParaType) => { para.isInputExpanded = this.state.selectedViewId === 'input_only'; para.paraRef = React.createRef(); @@ -200,7 +193,7 @@ export class Notebook extends Component { paragraphId: para.uniqueId, }, }) - .then((res) => { + .then((_res) => { const paragraphs = [...this.state.paragraphs]; paragraphs.splice(index, 1); const parsedPara = [...this.state.parsedPara]; @@ -212,6 +205,7 @@ export class Notebook extends Component { 'Error deleting paragraph, please make sure you have the correct permission.', 'danger' ); + console.error(err); }); } }; @@ -253,6 +247,7 @@ export class Notebook extends Component { 'Error deleting paragraph, please make sure you have the correct permission.', 'danger' ); + console.error(err); }); }, 'Delete all paragraphs', @@ -361,6 +356,7 @@ export class Notebook extends Component { 'Error deleting visualization, please make sure you have the correct permission.', 'danger' ); + console.error(err); }); }; @@ -395,6 +391,7 @@ export class Notebook extends Component { 'Error adding paragraph, please make sure you have the correct permission.', 'danger' ); + console.error(err); }); }; @@ -428,13 +425,14 @@ export class Notebook extends Component { .post(`${NOTEBOOKS_API_PREFIX}/set_paragraphs/`, { body: JSON.stringify(moveParaObj), }) - .then((res) => this.setState({ paragraphs, parsedPara })) - .then((res) => this.scrollToPara(targetIndex)) + .then((_res) => this.setState({ paragraphs, parsedPara })) + .then((_res) => this.scrollToPara(targetIndex)) .catch((err) => { this.props.setToast( 'Error moving paragraphs, please make sure you have the correct permission.', 'danger' ); + console.error(err); }); }; @@ -467,6 +465,7 @@ export class Notebook extends Component { 'Error clearing paragraphs, please make sure you have the correct permission.', 'danger' ); + console.error(err); }); }; @@ -537,9 +536,9 @@ export class Notebook extends Component { } }; - runForAllParagraphs = (reducer: (para: ParaType, index: number) => Promise) => { + runForAllParagraphs = (reducer: (para: ParaType, _index: number) => Promise) => { return this.state.parsedPara - .map((para: ParaType, index: number) => () => reducer(para, index)) + .map((para: ParaType, _index: number) => () => reducer(para, _index)) .reduce((chain, func) => chain.then(func), Promise.resolve()); }; @@ -595,6 +594,7 @@ export class Notebook extends Component { 'Error fetching notebooks, please make sure you have the correct permission.', 'danger' ); + console.error(err); }); }; @@ -611,6 +611,7 @@ export class Notebook extends Component { }) .catch((err) => { this.props.setToast('Error getting query output', 'danger'); + console.error(err); }); }; @@ -662,6 +663,7 @@ export class Notebook extends Component { }) .catch((error) => { this.props.setToast('Error checking Reporting Plugin Installation status.', 'danger'); + console.error(error); }); } @@ -696,7 +698,7 @@ export class Notebook extends Component {

); - const viewOptions: EuiButtonGroupOption[] = [ + const viewOptions: EuiButtonGroupOptionProps[] = [ { id: 'view_both', label: 'View both', @@ -750,7 +752,7 @@ export class Notebook extends Component { disabled: this.state.parsedPara.length === 0, onClick: () => { this.setState({ isParaActionsPopoverOpen: false }); - this.runForAllParagraphs((para: ParaType, index: number) => { + this.runForAllParagraphs((para: ParaType, _index: number) => { return para.paraRef.current?.runParagraph(); }); if (this.state.selectedViewId === 'input_only') { @@ -854,7 +856,7 @@ export class Notebook extends Component { items: [ { name: 'Download PDF', - icon: , + icon: , onClick: () => { this.setState({ isReportingActionsPopoverOpen: false }); generateInContextReport('pdf', this.props, this.toggleReportingLoadingModal); @@ -897,7 +899,11 @@ export class Notebook extends Component { id="reportingActionsButton" iconType="arrowDown" iconSide="right" - onClick={() => this.setState({ isReportingActionsPopoverOpen: true })} + onClick={() => + this.setState({ + isReportingActionsPopoverOpen: !this.state.isReportingActionsPopoverOpen, + }) + } > Reporting actions @@ -929,6 +935,7 @@ export class Notebook extends Component { onChange={(id) => { this.updateView(id); }} + legend="notebook view buttons" /> )} @@ -942,7 +949,11 @@ export class Notebook extends Component { data-test-subj="notebook-paragraph-actions-button" iconType="arrowDown" iconSide="right" - onClick={() => this.setState({ isParaActionsPopoverOpen: true })} + onClick={() => + this.setState({ + isParaActionsPopoverOpen: !this.state.isParaActionsPopoverOpen, + }) + } > Paragraph actions @@ -962,7 +973,11 @@ export class Notebook extends Component { data-test-subj="notebook-notebook-actions-button" iconType="arrowDown" iconSide="right" - onClick={() => this.setState({ isNoteActionsPopoverOpen: true })} + onClick={() => + this.setState({ + isNoteActionsPopoverOpen: !this.state.isNoteActionsPopoverOpen, + }) + } > Notebook actions diff --git a/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/para_output.test.tsx.snap b/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/para_output.test.tsx.snap index a149d56beb..029f9e027c 100644 --- a/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/para_output.test.tsx.snap +++ b/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/para_output.test.tsx.snap @@ -1,5 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[` spec renders dashboards visualization outputs 1`] = ` +
+ 2020-Jul-21 18:37:44 - 2020-Aug-20 18:37:44 +
+`; + exports[` spec renders markdown outputs 1`] = `
spec renders markdown outputs 1`] = `
`; +exports[` spec renders observability visualization outputs 1`] = ` +
+ 2020-Jul-21 18:37:44 - 2020-Aug-20 18:37:44 +
+`; + exports[` spec renders other types of outputs 1`] = `
spec renders query outputs 1`] = `
`; -exports[` spec renders visualization outputs 1`] = ` +exports[` spec renders query outputs with error 1`] = `
- 2020-07-21T18:37:44+00:00 - 2020-08-20T18:37:44+00:00 +
+    
+      {"error":"Invalid SQL query"}
+    
+  
`; diff --git a/public/components/notebooks/components/paragraph_components/__tests__/para_input.test.tsx b/public/components/notebooks/components/paragraph_components/__tests__/para_input.test.tsx index de07ab37d0..4841064a03 100644 --- a/public/components/notebooks/components/paragraph_components/__tests__/para_input.test.tsx +++ b/public/components/notebooks/components/paragraph_components/__tests__/para_input.test.tsx @@ -7,7 +7,7 @@ import { fireEvent, render, waitFor } from '@testing-library/react'; import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; -import { sampleParsedParagraghs1 } from '../../helpers/__tests__/sampleDefaultNotebooks'; +import { sampleParsedParagraghs1 } from '../../../../../../test/notebooks_constants'; import { ParaInput } from '../para_input'; describe(' spec', () => { diff --git a/public/components/notebooks/components/paragraph_components/__tests__/para_output.test.tsx b/public/components/notebooks/components/paragraph_components/__tests__/para_output.test.tsx index 167daf25a2..a7f35a6a07 100644 --- a/public/components/notebooks/components/paragraph_components/__tests__/para_output.test.tsx +++ b/public/components/notebooks/components/paragraph_components/__tests__/para_output.test.tsx @@ -3,15 +3,28 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { fireEvent, render } from '@testing-library/react'; -import { configure, mount, shallow } from 'enzyme'; +import { render } from '@testing-library/react'; +import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; -import { sampleParsedParagraghs1 } from '../../helpers/__tests__/sampleDefaultNotebooks'; +import { Provider } from 'react-redux'; +import { legacy_createStore as createStore } from 'redux'; +import { + getOSDHttp, + setPPLService, + uiSettingsService, +} from '../../../../../../common/utils/core_services'; +import { + sampleObservabilityVizParagraph, + sampleParsedParagraghs1, +} from '../../../../../../test/notebooks_constants'; +import { rootReducer } from '../../../../../framework/redux/reducers'; +import PPLService from '../../../../../services/requests/ppl'; import { ParaOutput } from '../para_output'; describe(' spec', () => { configure({ adapter: new Adapter() }); + const store = createStore(rootReducer); it('renders markdown outputs', () => { const para = sampleParsedParagraghs1[0]; @@ -45,21 +58,65 @@ describe(' spec', () => { expect(utils.container.firstChild).toMatchSnapshot(); }); - it('renders visualization outputs', () => { + it('renders query outputs with error', () => { + const para = sampleParsedParagraghs1[3]; + para.out = ['{"error":"Invalid SQL query"}']; + para.isSelected = true; + const setVisInput = jest.fn(); + const utils = render( + + ); + expect(utils.container.firstChild).toMatchSnapshot(); + }); + + it('renders dashboards visualization outputs', () => { const para = sampleParsedParagraghs1[2]; para.isSelected = true; + + uiSettingsService.get = jest.fn().mockReturnValue('YYYY-MMM-DD HH:mm:ss'); const setVisInput = jest.fn(); const utils = render( null} /> ); + expect(utils.container.textContent).toMatch('2020-Jul-21 18:37:44 - 2020-Aug-20 18:37:44'); + expect(utils.container.firstChild).toMatchSnapshot(); + }); + + it('renders observability visualization outputs', () => { + setPPLService(new PPLService(getOSDHttp())); + const para = sampleObservabilityVizParagraph; + para.isSelected = true; + + uiSettingsService.get = jest.fn().mockReturnValue('YYYY-MMM-DD HH:mm:ss'); + const setVisInput = jest.fn(); + const utils = render( + + null} + /> + + ); + expect(utils.container.textContent).toMatch('2020-Jul-21 18:37:44 - 2020-Aug-20 18:37:44'); expect(utils.container.firstChild).toMatchSnapshot(); }); diff --git a/public/components/notebooks/components/paragraph_components/__tests__/paragraphs.test.tsx b/public/components/notebooks/components/paragraph_components/__tests__/paragraphs.test.tsx index 710499a998..f2b054ff3a 100644 --- a/public/components/notebooks/components/paragraph_components/__tests__/paragraphs.test.tsx +++ b/public/components/notebooks/components/paragraph_components/__tests__/paragraphs.test.tsx @@ -3,12 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { fireEvent, render } from '@testing-library/react'; -import { configure, mount, shallow } from 'enzyme'; +import { render } from '@testing-library/react'; +import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; -import httpClientMock from '../../../../../../test/__mocks__/httpClientMock'; -import { sampleParsedParagraghs1 } from '../../helpers/__tests__/sampleDefaultNotebooks'; +import { getOSDHttp } from '../../../../../../common/utils'; +import { sampleParsedParagraghs1 } from '../../../../../../test/notebooks_constants'; import { Paragraphs } from '../paragraphs'; jest.mock('../../../../../../../../src/plugins/embeddable/public', () => ({ @@ -50,7 +50,7 @@ describe(' spec', () => { addPara={addPara} DashboardContainerByValueRenderer={DashboardContainerByValueRenderer} deleteVizualization={deleteVizualization} - http={httpClientMock} + http={getOSDHttp()} selectedViewId="view_both" setSelectedViewId={setSelectedViewId} deletePara={deletePara} diff --git a/public/components/notebooks/components/paragraph_components/para_output.tsx b/public/components/notebooks/components/paragraph_components/para_output.tsx index 1102e98282..6fee9c66c3 100644 --- a/public/components/notebooks/components/paragraph_components/para_output.tsx +++ b/public/components/notebooks/components/paragraph_components/para_output.tsx @@ -7,16 +7,16 @@ import { EuiCodeBlock, EuiSpacer, EuiText } from '@elastic/eui'; import MarkdownRender from '@nteract/markdown'; import { Media } from '@nteract/outputs'; import moment from 'moment'; -import React, { useState } from 'react'; -import { VisualizationContainer } from '../../../../components/custom_panels/panel_modules/visualization_container'; -import PPLService from '../../../../services/requests/ppl'; +import React from 'react'; import { CoreStart } from '../../../../../../../src/core/public'; import { DashboardContainerInput, DashboardStart, } from '../../../../../../../src/plugins/dashboard/public'; import { ParaType } from '../../../../../common/types/notebooks'; -import { uiSettingsService } from '../../../../../common/utils'; +import { getOSDHttp, getPPLService, uiSettingsService } from '../../../../../common/utils'; +import { VisualizationContainer } from '../../../../components/custom_panels/panel_modules/visualization_container'; +import PPLService from '../../../../services/requests/ppl'; import { QueryDataGridMemo } from './para_query_grid'; const createQueryColumns = (jsonColumns: any[]) => { @@ -53,44 +53,19 @@ const getQueryOutputData = (queryObject: any) => { return data; }; -const QueryPara = ({ inp, val }) => { - const inputQuery = inp.substring(4, inp.length); - const queryObject = JSON.parse(val); - - const columns = createQueryColumns(queryObject.schema); - const [visibleColumns, setVisibleColumns] = useState(columns.map((c) => c.id)); - const data = getQueryOutputData(queryObject); - - return queryObject.hasOwnProperty('error') ? ( - {val} - ) : ( -
- - {inputQuery} - - - -
- ); -}; - const OutputBody = ({ + key, typeOut, val, - inp, + para, visInput, setVisInput, DashboardContainerByValueRenderer, }: { + key: string; typeOut: string; val: string; - inp: string; + para: ParaType; visInput: DashboardContainerInput; setVisInput: (input: DashboardContainerInput) => void; DashboardContainerByValueRenderer: DashboardStart['DashboardContainerByValueRenderer']; @@ -99,15 +74,37 @@ const OutputBody = ({ * Currently supports HTML, TABLE, IMG * TODO: add table rendering */ + const dateFormat = uiSettingsService.get('dateFormat'); if (typeOut !== undefined) { switch (typeOut) { case 'QUERY': - return ; + const inputQuery = para.inp.substring(4, para.inp.length); + const queryObject = JSON.parse(val); + if (queryObject.hasOwnProperty('error')) { + return {val}; + } else { + const columns = createQueryColumns(queryObject.schema); + const data = getQueryOutputData(queryObject); + return ( +
+ + {inputQuery} + + + +
+ ); + } case 'MARKDOWN': return ( - + ); @@ -121,7 +118,11 @@ const OutputBody = ({ {`${from} - ${to}`} - + ); case 'OBSERVABILITY_VISUALIZATION': @@ -139,35 +140,34 @@ const OutputBody = ({
); case 'HTML': return ( - + {/* eslint-disable-next-line react/jsx-pascal-case */} ); case 'TABLE': - return
{val}
; + return
{val}
; case 'IMG': - return ; + return ; default: - return
{val}
; + return
{val}
; } } else { console.log('output not supported', typeOut); @@ -194,21 +194,23 @@ export const ParaOutput = (props: { }) => { const { para, DashboardContainerByValueRenderer, visInput, setVisInput } = props; - return !para.isOutputHidden ? ( - <> - {para.typeOut.map((typeOut: string, tIdx: number) => { - return ( - - ); - })} - - ) : null; + return ( + !para.isOutputHidden && ( + <> + {para.typeOut.map((typeOut: string, tIdx: number) => { + return ( + + ); + })} + + ) + ); }; diff --git a/public/components/notebooks/components/paragraph_components/para_query_grid.tsx b/public/components/notebooks/components/paragraph_components/para_query_grid.tsx index 6321c57026..28b986076a 100644 --- a/public/components/notebooks/components/paragraph_components/para_query_grid.tsx +++ b/public/components/notebooks/components/paragraph_components/para_query_grid.tsx @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import $ from 'jquery'; import { EuiDataGrid, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import $ from 'jquery'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; interface QueryDataGridProps { rowCount: number; @@ -23,7 +23,7 @@ function QueryDataGrid(props: QueryDataGridProps) { const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); // ** Sorting config const [sortingColumns, setSortingColumns] = useState([]); - const [visibleColumns, setVisibleColumns] = useState>([]); + const [visibleColumns, setVisibleColumns] = useState([]); const [isVisible, setIsVisible] = useState(false); @@ -108,7 +108,6 @@ function queryDataGridPropsAreEqual(prevProps: QueryDataGridProps, nextProps: Qu return ( prevProps.rowCount === nextProps.rowCount && JSON.stringify(prevProps.queryColumns) === JSON.stringify(nextProps.queryColumns) && - JSON.stringify(prevProps.visibleColumns) === JSON.stringify(nextProps.visibleColumns) && JSON.stringify(prevProps.dataValues) === JSON.stringify(nextProps.dataValues) ); } diff --git a/public/components/notebooks/docs/dev/Zeppelin_backend_adaptor.md b/public/components/notebooks/docs/dev/Zeppelin_backend_adaptor.md deleted file mode 100644 index 054c1a886e..0000000000 --- a/public/components/notebooks/docs/dev/Zeppelin_backend_adaptor.md +++ /dev/null @@ -1,129 +0,0 @@ -# Zeppelin Backend Adaptor - -## Contents - -1. [**Zeppelin Backend Service**](#zeppelin-backend-service) -2. [**Apache Zeppelin Setup**](#apache-zeppelin-setup) - -## Zeppelin Backend Service - -**Apache Zeppelin** provides several REST APIs for interaction and remote activation of zeppelin functionality. All REST APIs are available starting with the following endpoint `http://[zeppelin-server]:[zeppelin-port]/api`. - -![Zeppelin Server](images/zeppelin_architecture.png) - -1. **APIs Provided:** - 1. **[Server:](http://zeppelin.apache.org/docs/0.9.0/usage/rest_api/zeppelin_server.html)** Get status, version, Log Level - 2. **[Interpreter:](http://zeppelin.apache.org/docs/0.9.0/usage/rest_api/interpreter.html)** Get interpreter settings, create/update/restart/delete interpreter setting - 3. **[Notebook:](http://zeppelin.apache.org/docs/0.9.0/usage/rest_api/notebook.html)** Create/update/restart/delete note and paragraph ops - 4. **[Repository:](http://zeppelin.apache.org/docs/0.9.0/usage/rest_api/notebook_repository.html)** Get/Update NB repo - 5. **[Configuration:](http://zeppelin.apache.org/docs/0.9.0/usage/rest_api/configuration.html)** Get all [Zeppelin config](http://zeppelin.apache.org/docs/0.9.0/setup/operation/configuration.html) - server port, ssl, S3 bucket, S3.user - 6. **[Credential:](http://zeppelin.apache.org/docs/0.9.0/usage/rest_api/credential.html)** List credentials for all users, create/delete - 7. **[Helium:](http://zeppelin.apache.org/docs/0.9.0/usage/rest_api/helium.html)** Contains APIs for all plugin packages (Not needed as of now) -2. **Security:** - 1. By default the APIs are exposed to anonymous user - 2. Recommended way to use **access control**: **[Shiro Auth](http://zeppelin.apache.org/docs/0.9.0/setup/security/shiro_authentication.html)** - 1. Need to change [**Shiro.ini (Apache link)**](http://shiro.apache.org/configuration.html#ini-sections) in conf directory - 2. Ideally should be used with [**Apache KnoxSSO**](https://knox.apache.org/books/knox-0-13-0/dev-guide.html#KnoxSSO+Integration) - 3. Also, [Notebooks](http://zeppelin.apache.org/docs/0.9.0/setup/security/notebook_authorization.html) can have access control based on Shiro defined users -3. **Deployment:** - 1. Recommended way is to use stand alone docker - 2. Create a **custom docker** with new Shiro & Zeppelin configs and set interpreter config for OpenSearch and OpenSearch-sql. - 3. Sample scripts available in `scripts/docker/spark-cluster-managers` -4. **Storage:** - 1. Apache Zeppelin has a pluggable notebook storage mechanism controlled by `zeppelin.notebook.storage` configuration option with multiple implementations. - 2. Zeppelin has** built-in S3/github connector**. Just provide credentials in [properties or env-sh](http://zeppelin.apache.org/docs/0.9.0/setup/storage/storage.html#notebook-storage-in-s3) - 3. The notebooks are automatically synced by Zeppelin - -## **Apache Zeppelin Setup** - -- https://zeppelin.apache.org/ -- Web-based notebook that enables data-driven, interactive data analytics and collaborative documents with SQL, Scala and more. -- **[Installation Steps](http://zeppelin.apache.org/docs/0.9.0/quickstart/install.html)** - - http://zeppelin.apache.org/download.html → Install using Binary package with all interpreters. - - Unpack the downloaded tar - - To Run the service use `bin/zeppelin-daemon.sh start` - - To Stop the service use `bin/zeppelin-daemon.sh stop` - - Service starts on port 8080 - - If on a remote server (like ec2) and want to use server IP to access the notebook: - - Make sure your inbound/outbound ports are set correctly on the remote machine - - You may want to change the Zeppelin host ip to 0.0.0.0 (or keep it localhost) - - To change the host ip use the `zeppelin-site.xml.template` inside “conf/“ directory - - `cp conf/zeppelin-site.xml.template conf/zeppelin-site.xml` - - `vi conf/zeppelin-site.xml` and edit the host ip - - Then restart the service -- **[Optional] Setup OpenSearch Interpreter:** - - - [Zeppelin OpenSearch interpreter Documentation](https://zeppelin.apache.org/docs/0.9.0/interpreter/elasticsearch.html) - - This interpreter can be used for OpenSearch: - - - **Note: current issues with OpenSearch Interpreter in Zeppelin** - - User needs to remove ssl flag from the OpenSearch config as Zeppelin doesn’t support ssl request yet: https://issues.apache.org/jira/browse/ZEPPELIN-2031 so run the OpenSearch service without ssl enabled - - Zeppelin has “no support for ssl” (only uses http) in elastic interpreter: - - [Code](https://github.com/apache/zeppelin/blob/0b8423c62ae52f3716d4bb63d60762fee6910788/elasticsearch/src/main/java/org/apache/zeppelin/elasticsearch/client/HttpBasedClient.java#L105) - - [Apache Issues](https://issues.apache.org/jira/browse/ZEPPELIN-2031) - - Zeppelin has “no issue in search query” in elastic interpreter: - - [Apache Issues](https://issues.apache.org/jira/browse/ZEPPELIN-4843?jql=project%20%3D%20ZEPPELIN%20AND%20status%20%3D%20Open%20AND%20text%20~%20%22elasticsearch%22) - - Zeppelin “No support for search template“ issue in elastic interpreter: - - [Apache Issues](https://issues.apache.org/jira/browse/ZEPPELIN-4184?jql=project%20%3D%20ZEPPELIN%20AND%20text%20~%20%22elastic%20search%22) - - **To Change the interpreter settings of Elastic Search Interpreter on Zeppelin:** - - Open Zeppelin in browser `localhost:8080` - - Please click the top right menu beside drop down and select interpreter option - - In the interpreters window, search for elasticsearch interpreter - - Edit config parameters similar to that your local/remote service - - You’ll be prompted to restart the interpreter -> Click on ok - - **If using the default settings, add below mentioned changes in interpreter config:** - - - Change transport.type to http - - host → localhost (if running on same machine as Zeppelin) & port → 9200 - - username: admin & password: admin - - Once configured the screen should look like this: - ![OpenSearch Interpreter](images/opensearch-zeppelin.png) - - - Start a new notebook to try out the below commands - - Run a shell command from notebook to check availability of OpenSearch: - - `%sh curl -XGET http://localhost:9200 -u admin:admin` - -``` -%elasticsearch -index movies/default/1 { - "title": "The Godfather", - "director": "Francis Ford Coppola", - "year": 1972, - "genres": ["Crime", "Drama"], - "rating":5 -} -``` - -- **[Optional] Setup OpenSearch-SQL JDBC Interpreter:** - - [Zeppelin JDBC Interpreter Documentation](https://zeppelin.apache.org/docs/0.9.0/interpreter/jdbc.html) - - Zeppelin has a generic JDBC interpreter, we can use this to add our OpenSearch-SQL Driver - - Download [OpenSearch-SQL Driver](https://opensearch.org/) Jar file - - To Use JDBC interpreter: - - **To add the JDBC interpreter settings for OpenSearch-SQL:** - - Open Zeppelin in browser `localhost:8080` - - Please click the top right menu beside drop down and select interpreter option - - Click on "+ Create" Button - - Add OpenSearch-SQL interpreter with type JDBC **configure name: “opensearchsql”** - - Note: The name you assign to the interpreter is used later for accessing paragraphs in notebook - “%opensearchsql” - - Edit config for with OpenSearch-SQL Driver details (Please refer to the [Github README](https://github.com/opensearch-project/sql/tree/main/sql-jdbc)) - - **If using the default settings, add below mentioned changes in interpreter config:** - - Edit the url: `jdbc:elasticsearch://localhost:9200` - - Edit the driver class: `org.opensearch.jdbc.Driver` - - Edit the username to admin - - Edit the password to admin - - Add absolute path to the Jar in the last input box - - You’ll be prompted to restart the interpreter -> Click on ok - - Once configured the screen should look like this: - ![SQL Interpreter](images/opensearch-zeppelin-settings.png) - - - Open a notebook and run below commands to check if interpreter settings are set correctly - -``` -%opensearchsql -SELECT * FROM movies -``` - -- **Appendix:** - - **[More on Zeppelin UI](http://zeppelin.apache.org/docs/latest/quickstart/explore_ui.html)** - - [**More on Zeppelin Config**](http://zeppelin.apache.org/docs/latest/setup/operation/configuration.html) - - [**S3-Notebook Storage**](http://zeppelin.apache.org/docs/0.8.2/setup/storage/storage.html#notebook-storage-in-s3) diff --git a/public/components/notebooks/docs/dev/images/zeppelin_architecture.png b/public/components/notebooks/docs/dev/images/zeppelin_architecture.png deleted file mode 100644 index cc1a64e70c..0000000000 Binary files a/public/components/notebooks/docs/dev/images/zeppelin_architecture.png and /dev/null differ diff --git a/public/components/notebooks/docs/example_notebooks/zeppelin/Introduction Notebook-Zeppelin.json b/public/components/notebooks/docs/example_notebooks/zeppelin/Introduction Notebook-Zeppelin.json deleted file mode 100644 index 90e3c157fe..0000000000 --- a/public/components/notebooks/docs/example_notebooks/zeppelin/Introduction Notebook-Zeppelin.json +++ /dev/null @@ -1 +0,0 @@ -{"paragraphs":[{"text":"%md \n\n## Hi Everyone,\n### Here's an intro to **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to create, rename, clone, delete, import and export with notebooks \n* Inner top left buttons are used to run, save, clone, delete the selected paragraph. \n* Long hover over the buttons to see Tooltip helpers","user":"anonymous","dateUpdated":"2020-08-21 00:55:58.360","config":{},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"HTML","data":"
\n

Hi Everyone,

\n

Here’s an intro to OpenSearch Dashboards Notebooks

\n
    \n
  • You may use the top left buttons to create, rename, clone, delete, import and export with notebooks
  • \n
  • Inner top left buttons are used to run, save, clone, delete the selected paragraph.
  • \n
  • Long hover over the buttons to see Tooltip helpers
  • \n
\n\n
"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597958104590_901298942","id":"paragraph_1596519508360_932236116","dateCreated":"2020-08-20 21:15:04.590","dateStarted":"2020-08-21 00:55:58.362","dateFinished":"2020-08-21 00:55:58.368","status":"FINISHED"},{"title":"CODE","text":"%md\n\n## Greetings!\n* Yay! you may import and export me as json files\n* **Run** a paragraph with a keyboard shortcut **\"Shift+Enter\"**\n* In Zeppelin each paragraph has to have a \"%[interpreter]\" header (like %md in this paragraph)","user":"anonymous","dateUpdated":"2020-08-21 01:08:56.942","config":{},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"HTML","data":"
\n

Greetings!

\n
    \n
  • Yay! you may import and export me as json files
  • \n
  • Run a paragraph with a keyboard shortcut “Shift+Enter”
  • \n
  • In Zeppelin each paragraph has to have a “%[interpreter]” header (like %md in this paragraph)
  • \n
\n\n
"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597972120089_1145163794","id":"paragraph_1597972120089_1145163794","dateCreated":"2020-08-21 01:08:40.089","dateStarted":"2020-08-21 01:08:56.944","dateFinished":"2020-08-21 01:08:56.952","status":"FINISHED"},{"title":"Paragraph inserted","text":"%md\n\n## Now that you are using Zeppelin \n* Checkout how to [setup other interpreters](https://zeppelin.apache.org/docs/0.9.0/#available-interpreters)\n* To setup Elasticsearch interpreter and OpenSearch-SQL interpreters [checkout](https://github.com/opensearch-project/dashboards-notebooks/blob/dev/docs/dev/Zeppelin_backend_adaptor.md#apache-zeppelin-setup)\n","user":"anonymous","dateUpdated":"2020-08-21 01:08:52.286","config":{},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"HTML","data":"
\n

Now that you are using Zeppelin

\n\n\n
"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597958104590_1715920734","id":"paragraph_1596742076640_674206137","dateCreated":"2020-08-20 21:15:04.590","dateStarted":"2020-08-21 01:08:52.288","dateFinished":"2020-08-21 01:08:52.295","status":"FINISHED"},{"title":"Paragraph inserted","text":"%md\n\n### Hover between paragraphs to add a Saved Visualization or a New Paragraph\n2. **Unpin** the container to *edit the size* or *delete it*\n3. **Refresh** the container after *date is changed*","user":"anonymous","dateUpdated":"2020-08-21 01:09:14.147","config":{},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"HTML","data":"
\n

Hover between paragraphs to add a Saved Visualization or a New Paragraph

\n
    \n
  1. Unpin the container to edit the size or delete it
  2. \n
  3. Refresh the container after date is changed
  4. \n
\n\n
"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597958104590_931410594","id":"paragraph_1596524302932_2112910756","dateCreated":"2020-08-20 21:15:04.590","dateStarted":"2020-08-21 01:09:14.149","dateFinished":"2020-08-21 01:09:14.160","status":"FINISHED"},{"title":"CODE","text":"%md\n\n# Start typing here","user":"anonymous","dateUpdated":"2020-08-21 00:56:41.827","config":{},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"HTML","data":"
\n

Start typing here

\n\n
"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597971380477_32105829","id":"paragraph_1597971380477_32105829","dateCreated":"2020-08-21 00:56:20.477","dateStarted":"2020-08-21 00:56:41.830","dateFinished":"2020-08-21 00:56:41.836","status":"FINISHED"}],"name":"Introduction Notebook-Zeppelin","id":"2FJH8PW8K","defaultInterpreterGroup":"spark","version":"0.9.0-preview2","noteParams":{},"noteForms":{},"angularObjects":{},"config":{"isZeppelinNotebookCronEnable":false},"info":{}} diff --git a/public/components/notebooks/docs/example_notebooks/zeppelin/Log Analysis-Zeppelin.json b/public/components/notebooks/docs/example_notebooks/zeppelin/Log Analysis-Zeppelin.json deleted file mode 100644 index 61571248e3..0000000000 --- a/public/components/notebooks/docs/example_notebooks/zeppelin/Log Analysis-Zeppelin.json +++ /dev/null @@ -1 +0,0 @@ -{"paragraphs":[{"title":"CODE","text":"%md\n## Let's use Zeppelin Adaptor to explore data with Inter-Para Communication\n* Before diving into this notebook make sure:\n * You have integrated [**Sample web logs**](https://www.elastic.co/guide/en/kibana/7.8/getting-started.html#get-data-in)\n * You have used the **Introduction Notebook**\n * You have setup a [**python interpreter**](https://zeppelin.apache.org/docs/0.9.0/interpreter/python.html) & [**OpenSearch-SQL interpreter**](https://github.com/opensearch-project/dashboards-notebooks/blob/dev/docs/dev/Zeppelin_backend_adaptor.md#apache-zeppelin-setup)","user":"anonymous","dateUpdated":"2020-08-21 01:07:36.617","config":{},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"HTML","data":"
\n

Let’s use Zeppelin Adaptor to explore data with Inter-Para Communication

\n\n\n
"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597970420851_2109527240","id":"paragraph_1597970420851_2109527240","dateCreated":"2020-08-21 00:40:20.851","dateStarted":"2020-08-21 01:07:36.619","dateFinished":"2020-08-21 01:07:36.626","status":"FINISHED"},{"title":"CODE","text":"%md\n## We'll use the pre-indexed sample web logs provided by OpenSearch Dashboards \n* Make an OpenSearch-SQL query \n* Import the output of SQL query in a python paragraph\n* Plot an anomaly graph using python-matplot","user":"anonymous","dateUpdated":"2020-08-21 00:58:02.620","config":{},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"HTML","data":"
\n

We’ll use the pre-indexed sample web logs provided by OpenSearch Dashboards

\n
    \n
  • Make an OpenSearch-SQL query
  • \n
  • Import the output of SQL query in a python paragraph
  • \n
  • Plot an anomaly graph using python-matplot
  • \n
\n\n
"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597971322703_978148592","id":"paragraph_1597971322703_978148592","dateCreated":"2020-08-21 00:55:22.703","dateStarted":"2020-08-21 00:58:02.629","dateFinished":"2020-08-21 00:58:02.634","status":"FINISHED"},{"title":"Paragraph inserted","text":"%md\n**OpenSearch-SQL Query to fetch size of request and agent data for all the web requests made**\nSelect bytes,agent from opensearch_dashboards_sample_data_logs\n","user":"anonymous","dateUpdated":"2020-08-20 21:16:13.576","config":{"editorSetting":{"language":"scala","editOnDblClick":false,"completionKey":"TAB","completionSupport":true},"colWidth":12,"editorMode":"ace/mode/scala","fontSize":9,"results":{},"enabled":true},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"HTML","data":"
\n

OpenSearch-SQL Query to fetch size of request and agent data for all the web requests made
\nSelect bytes,agent from opensearch_dashboards_sample_data_logs

\n\n
"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597958173576_437396150","id":"paragraph_1591897831553_-310393619","dateCreated":"2020-08-20 21:16:13.576","status":"READY"},{"title":"Paragraph inserted","text":"%opensearchsql(saveAs=data_logs)\nselect bytes,agent from opensearch_dashboards_sample_data_logs","user":"anonymous","dateUpdated":"2020-08-20 21:16:13.576","config":{"saveAs":"opensearch_dashboards_sample_data_logs","editorSetting":{"language":"sql","editOnDblClick":false,"completionKey":"TAB","completionSupport":true},"colWidth":12,"editorMode":"ace/mode/sql","fontSize":9,"results":{"0":{"graph":{"mode":"table","height":300,"optionOpen":false,"setting":{"table":{"tableGridState":{},"tableColumnTypeState":{"names":{"bytes":"string","agent":"string"},"updated":false},"tableOptionSpecHash":"[{\"name\":\"useFilter\",\"valueType\":\"boolean\",\"defaultValue\":false,\"widget\":\"checkbox\",\"description\":\"Enable filter for columns\"},{\"name\":\"showPagination\",\"valueType\":\"boolean\",\"defaultValue\":false,\"widget\":\"checkbox\",\"description\":\"Enable pagination for better navigation\"},{\"name\":\"showAggregationFooter\",\"valueType\":\"boolean\",\"defaultValue\":false,\"widget\":\"checkbox\",\"description\":\"Enable a footer for displaying aggregated values\"}]","tableOptionValue":{"useFilter":false,"showPagination":false,"showAggregationFooter":false},"updated":false,"initialized":false}},"commonSetting":{}}}},"enabled":true},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"TABLE","data":"bytes\tagent\n6219\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n6850\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n0\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n14113\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n2492\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n0\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n1872\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n4531\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n3629\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n9797\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9920\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n6936\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n8489\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n2860\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n8535\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n4529\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9888\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n5919\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9890\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n3039\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n8766\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n8261\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n5028\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n8130\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9934\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n3314\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n2492\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n1950\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n0\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n8489\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9029\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n2860\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n8120\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n3930\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n0\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n3464\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n8535\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n17403\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9773\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n875\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n18082\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n6514\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n8323\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n8364\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n6274\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n2108\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n7174\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n5846\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n7594\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n7169\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n9338\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9217\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n8390\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n9101\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n6936\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n4634\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n8909\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n7343\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n4939\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n55\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n1778\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n8996\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n5197\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n2153\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n5223\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n178\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n5400\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n8676\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n6960\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n18409\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n2441\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n3010\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n9263\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n3853\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n4238\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n2377\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n8928\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n6193\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n2372\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n6942\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n173\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n4877\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n15894\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n7468\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n5481\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n6920\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9920\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n5476\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n2432\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n2034\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n9021\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n7719\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n4037\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n17598\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n6312\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n5835\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n2647\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n8655\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n17357\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n4064\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n3034\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n1634\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n3807\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n8738\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n3629\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n9446\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n7182\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n2159\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n4861\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n3317\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n8663\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n1793\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n6648\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n3307\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n5052\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n4531\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n8995\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n4579\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n8522\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n7304\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n255\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9052\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n6795\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n6739\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n7936\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n7309\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n6254\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n2453\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n3378\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n9375\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n1685\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n0\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n0\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n4154\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n5919\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n4633\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n3039\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n5375\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n5424\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n14113\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n3841\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n1638\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n0\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n5861\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n3994\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n1828\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n4529\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n5321\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n15990\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n4806\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n4072\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n4617\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n9486\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9888\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n7193\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n0\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n6429\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n8648\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n7377\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n9371\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n8590\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n2765\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9424\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n0\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9716\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n1858\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n2432\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n9797\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n2999\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n6817\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n4842\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n16227\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n1603\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n19561\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n1936\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n5540\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n7085\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n5767\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n2053\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n4061\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n7675\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n0\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n979\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n685\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n6509\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n8723\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n9414\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n3086\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n1872\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9174\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n5073\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n10103\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n7531\tMozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\n5988\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n7009\tMozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\n6540\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n0\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n9952\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n7873\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n3050\tMozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1\n"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597958173576_876823109","id":"paragraph_1591897481776_702487776","dateCreated":"2020-08-20 21:16:13.576","status":"READY"},{"title":"Paragraph inserted","text":"%md\n**Import this query output in python**\n","user":"anonymous","dateUpdated":"2020-08-20 21:16:13.577","config":{"tableHide":false,"editorSetting":{"language":"markdown","editOnDblClick":true,"completionSupport":false},"colWidth":12,"editorMode":"ace/mode/markdown","fontSize":9,"editorHide":true,"results":{},"enabled":true},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"HTML","data":"
\n

Import this query output in python

\n\n
"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597958173576_599341048","id":"paragraph_1591897902017_-1544489575","dateCreated":"2020-08-20 21:16:13.577","status":"READY"},{"title":"Paragraph inserted","text":"%python.ipython\n%matplotlib inline\nlogs = z.getAsDataFrame('data_logs')\nlogs","user":"anonymous","dateUpdated":"2020-08-20 21:16:13.577","config":{"editorSetting":{"language":"scala","editOnDblClick":false,"completionKey":"TAB","completionSupport":true},"colWidth":12,"editorMode":"ace/mode/scala","fontSize":9,"results":{},"enabled":true},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"HTML","data":"
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
bytesagent
06219Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Geck...
16850Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Geck...
20Mozilla/5.0 (X11; Linux i686) AppleWebKit/534....
314113Mozilla/4.0 (compatible; MSIE 6.0; Windows NT ...
42492Mozilla/4.0 (compatible; MSIE 6.0; Windows NT ...
.........
1956540Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Geck...
1960Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Geck...
1979952Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Geck...
1987873Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Geck...
1993050Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Geck...
\n

200 rows × 2 columns

\n
"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597958173577_325755404","id":"paragraph_1591898708896_1841265952","dateCreated":"2020-08-20 21:16:13.577","status":"READY"},{"title":"Paragraph inserted","text":"\n%md\n**Plot a scatter graph for requests made per browser using matplot**\n","user":"anonymous","dateUpdated":"2020-08-20 21:16:13.577","config":{"editorSetting":{"language":"scala","editOnDblClick":false,"completionKey":"TAB","completionSupport":true},"colWidth":12,"editorMode":"ace/mode/scala","fontSize":9,"results":{},"enabled":true},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"HTML","data":"
\n

Plot a scatter graph for requests made per browser using matplot

\n\n
"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597958173577_2096035472","id":"paragraph_1591899582298_-864868294","dateCreated":"2020-08-20 21:16:13.577","status":"READY"},{"title":"Paragraph inserted","text":"%python.ipython\n\n%matplotlib inline\n\nimport warnings\nimport numpy as np\nwarnings.filterwarnings(\"ignore\")\nimport matplotlib.pyplot as plt\n\nlogs = z.getAsDataFrame('data_logs')\nagent_with_firefox = logs['agent'].str.count('Firefox').values\nfirefox_bytes = logs['bytes'].values[agent_with_firefox==1]\n\nagent_with_chrome_safari = logs['agent'].str.count('Chrome').values\nchrome_bytes = logs['bytes'].values[agent_with_chrome_safari==1]\n\nagent_with_msie = logs['agent'].str.count('MSIE').values\nmsie_bytes = logs['bytes'].values[agent_with_msie==1]\n\nprint(\"Total Requests:\", len(agent_with_firefox))\nprint(\"# Requests from Firefox Browser\", np.sum(agent_with_firefox))\nprint(\"# Requests from Chrome/Safari Browser\", np.sum(agent_with_chrome_safari))\nprint(\"# Requests from MSIE Browser\", np.sum(agent_with_msie), \"\\n\\n\")\n\nplt.figure(num=None, figsize=(30, 6))\nplt.title(\"Request size for each browser\")\nplt.ylabel('Request Size in Bytes')\nplt.xlabel('Requests made')\nplt.axhline(y=10000, color='r', linestyle='--')\nplt.plot(msie_bytes, 'ro', markersize=5, label=\"MSIE\")\nplt.plot(firefox_bytes, 'bo', markersize=5, label=\"Firefox\")\nplt.plot(chrome_bytes, 'go', markersize=5, label=\"Chrome\")\nplt.legend()\n\n\n\n\n \n","user":"anonymous","dateUpdated":"2020-08-20 21:16:13.577","config":{"editorSetting":{"language":"python","editOnDblClick":false,"completionKey":"TAB","completionSupport":true},"colWidth":12,"editorMode":"ace/mode/python","fontSize":9,"results":{},"enabled":true},"settings":{"params":{},"forms":{}},"results":{"code":"SUCCESS","msg":[{"type":"TEXT","data":"Total Requests: 200\n# Requests from Firefox Browser 86\n# Requests from Chrome/Safari Browser 54\n# Requests from MSIE Browser 60 \n\n\n\n"},{"type":"IMG","data":"iVBORw0KGgoAAAANSUhEUgAABs0AAAGDCAYAAABgLF6CAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nOzdfXxcZZ3w/8+3U0oxEQSsAq3YSkEBhUSKcXVRlIc4GB5cWaEKVFll8WHVvX1kb9Zqyy56q/f6w1VWXAERtuINPmB1NiBaQIVoamJBUShYpaWFCiuQ8FA6vX5/nJMyadMkbWcyk+Tzfr3mdc65ztP3zJxMzpzvua4rUkpIkiRJkiRJkiRJk9mUegcgSZIkSZIkSZIk1ZtJM0mSJEmSJEmSJE16Js0kSZIkSZIkSZI06Zk0kyRJkiRJkiRJ0qRn0kySJEmSJEmSJEmTnkkzSZIkSZIkSZIkTXomzSRJkiRpjEXEURHx+xps94KI+HNErKv2tqslIi6PiAtGuezsiEgRMbXWcUmSJEmSSTNJkiRJDSEiVkXEExHRFxHr8uRKc73j2paIODoiVu/IuimlW1JKL65yPC8APgQcklLap5rbliRJkqTJwKSZJEmSpEZyYkqpGWgBWoHz6hzPePJC4KGU0oPbu+JEqcnVaMcREYV6xyBJkiRp9EyaSZIkSWo4KaV1QCdZ8gyAiNg1Ij4XEX+KiAci4j8iYreK+R+JiLURcX9EnJ036zc3n7csIt5ZsezbI+KnFdMviYgbIuLhiPh9RLylYt4JEfHbiHgsItZExIcjogkoAfvlNeP6ImK/LY9jqHXz8s211CLitIpt9EXEUxGxbDTHXLGfY4EbKuK5PC8/KSJ+ExF/yd+DgyvWWRURH4uIFUD/UAmnEd6XN0ZET0Q8GhH3RcQnt1j3ryPi5/m+74uIt1fM3jMifpC/L10RccCW+97C2fnnujYiPlSxj09GxDURcWVEPAq8PX/PvpAvf38+vmu+/E0R8eaK+FJEnDDwHkZEbz4+N1/2kby5y6tH+Z5cHhEXR8QPI6IfeN0IxyVJkiSpgZg0kyRJktRwImIWUARWVhR/BjiILJE2F5gJfCJf/g3Ah4HjgAOBY7djX01kCaf/Ap4HzAe+HBGH5ot8Dfj7lNKzgZcCP04p9efx3Z9Sas5f9w+x+a3W3XKBlNLVA9sA9gPuBZaMdMxbbONHW8Tz9og4KN/OB4EZwA+B70fEtIpV5wNvBJ6TUtq4ne9LP3AW8Jx8G++OiFPydfcnSyp+Md93C9C7xX4/BexJ9hn/yxDvXaXXkX2uxwMfz5OEA04GrsnjuAr438Ar830eDrwCOD9f9ibg6Hz8NWTv9Wsrpm/KxxcD1+fxzcqPYzTvCcBb8+N5NvBTJEmSJI0bJs0kSZIkNZLvRsRjwH3Ag8BCgIgI4F3AP6aUHk4pPQb8K3B6vt5bgMtSSnfkCa1Pbsc+O4BVKaXLUkobU0q/Aq4FTs3nPw0cEhG7p5T+J58/WqNeNyKmkCVjlqWUvjKKYx7JacAPUko3pJSeBj4H7Aa8qmKZi1JK96WUnhhi/WHfl5TSspTS7SmlTSmlFWQJuoEE1NuAH6WUlqSUnk4pPZRSqkyafTul9Is8UXcVFTUKt+FTKaX+lNLtwGVkyaoBt6aUvpvH8US+70UppQdTSuvJknNn5svexOAk2YUV06/lmaTZ02TNXe6XUnoypTSQ/BrpXAH4XkrpZ3k8T45wXJIkSZIaiEkzSZIkSY3klLxW1tHAS4Dn5uUzgGcBy/Pm/v4C/HdeDlkNrfsqtvPH7djnC4G2ge3m234bsE8+/83ACcAf8yb7/mo7tr096w7UTnp/Pj3SMY9kPyreh5TSJrL3aGbFMvdtuVKFYd+XiGiLiJ9ExPqIeAQ4l2c+rxcA9wyz7XUV448DzSMcy5af7X7bmAdbHPcWy98KHBQRzydL1F0BvCAinktWI+3mfLmPAgH8Im/e8uy8fKRzZah4JEmSJI0TDdVJsiRJkiQBpJRuyvvl+hxwCvBn4Ang0JTSmiFWWUuWqBmw/xbz+8kSUAO2THLclFI6bhux/BI4OSJ2Ad4HfCvfVxrFcWxr3UEi4nSy2lNH5rXCYORjHsn9wMsq9hH5viu3NdwxDPu+kNWK+3egmFJ6MiK+wDNJs/vIklDV8gLgd/n4/mTHNmDLY7ifLLn1my2XTyk9HhHLgQ8Ad6SUNkTEz4H/BdyTUvpzvtw6slp+RMRfAz+KiJsZ+T0ZKh5JkiRJ44Q1zSRJkiQ1qi8Ax0VES15L6qvAv0XE8wAiYmZEtOfLfgt4e0QcEhHPIm/WsUIv8DcR8ayImAv8XcW8pWS1j86MiF3y15ERcXBETIuIt0XEHnky61GgnK/3ALB3ROwxVPAjrFu5XCtZn1mn5M0JAptrhg13zCP5FvDGiDgmT9p9CHgK+Pko19/m+5LPfzbwcJ4wewVZX14DrgKOjYi3RMTUiNg7IkZqgnE4/5x/docC7wCuHmbZJcD5ETEjr0H2CeDKivk3kSUwB5piXLbFNBHxt3m/egD/Q5YIKzPyeyJJkiRpHDNpJkmSJKkh5QmkK4B/zos+BqwEbouIR4EfAS/Oly2RJdl+nC/z4y0292/ABrJE19fJkjoD+3kMOJ6sr7D7yZoO/Aywa77ImcCqfJ/nAmfk6/2OLEFzb95UX2WTgQy37hZOBvYEfhoRffmrNNIxjySl9Pt8f18kq7V2InBiSmnDKNcf6X15D7Ao74PuE2RJuoF1/0TWLOWHgIfJkpaHj2a/23AT2ftwI/C5lNL1wyx7AdANrABuB36Vl1Vu69k80xTjltMARwJdEdEHXAd8IKX0h1G8J5IkSZLGsUjJliMkSZIkTTwRkYADU0or6x2LJEmSJKnxWdNMkiRJkiRJkiRJk55JM0mSJEmSJEmSJE16Ns8oSZIkSZIkSZKkSc+aZpIkSZIkSZIkSZr0TJpJkiRJkiRJkiRp0pta7wDG2nOf+9w0e/bseochSZIkSZIkSZKkMbZ8+fI/p5RmDDVv0iXNZs+eTXd3d73DkCRJkiRJkiRJ0hiLiD9ua57NM0qSJEmSJEmSJGnSM2kmSZIkSZIkSZKkSa9mSbOIeEFE/CQi7oyI30TEB/LyvSLihoi4Ox/umZdHRFwUESsjYkVEvLxiWwvy5e+OiAUV5UdExO35OhdFRNTqeCRJkiRJkiRJkjRx1bJPs43Ah1JKv4qIZwPLI+IG4O3AjSmlT0fEx4GPAx8DisCB+asNuBhoi4i9gIXAPCDl27kupfQ/+TLnALcBPwTeAJRqeEySJEmSJEmSJEl18/TTT7N69WqefPLJeofS0KZPn86sWbPYZZddRr1OzZJmKaW1wNp8/LGIuBOYCZwMHJ0v9nVgGVnS7GTgipRSAm6LiOdExL75sjeklB4GyBNvb4iIZcDuKaVb8/IrgFMwaSZJkiRJkiRJkiao1atX8+xnP5vZs2djA3xDSynx0EMPsXr1aubMmTPq9cakT7OImA20Al3A8/OE2kBi7Xn5YjOB+ypWW52XDVe+eohySZIkSZIkSZKkCenJJ59k7733NmE2jIhg77333u7aeDVPmkVEM3At8MGU0qPDLTpEWdqB8qFiOCciuiOie/369SOFLEmSJEmSJEmS1LBMmI1sR96jmibNImIXsoTZVSmlb+fFD+TNLpIPH8zLVwMvqFh9FnD/COWzhijfSkrpkpTSvJTSvBkzZuzcQUmSJEmSJEmSJE1iEcGZZ565eXrjxo3MmDGDjo4OAB544AE6Ojo4/PDDOeSQQzjhhBMAWLVqFS996UsBWLZsGXvssQctLS2bXz/60Y/G/mAq1KxPs8hSeF8D7kwp/d+KWdcBC4BP58PvVZS/LyK+CbQBj6SU1kZEJ/CvEbFnvtzxwHkppYcj4rGIeCVZs49nAV+s1fFIkiRJkiRJkiSNO+UylErQ0wOtrVAsQqGwU5tsamrijjvu4IknnmC33XbjhhtuYObMZ3rQ+sQnPsFxxx3HBz7wAQBWrFgx5HaOOuooli5dulOxVFMta5q9GjgTeH1E9OavE8iSZcdFxN3Acfk0wA+Be4GVwFeB9wCklB4GFgO/zF+L8jKAdwP/ma9zD1Cq4fFIkiRJkiap8qYyS+9ayuKbFrP0rqWUN5XrHZIkSZI0snIZ2tth/nxYuDAbtrdn5TupWCzygx/8AIAlS5Ywf/78zfPWrl3LrFnPNBZ42GGH7fT+xkLNapqllH7K0P2OARwzxPIJeO82tnUpcOkQ5d3AS3ciTEmSJEmShlXeVKb9yna61nTRv6GfpmlNtM1so/OMTgpTdu4JXUmSJKmmSiXo6oK+vmy6ry+bLpUgb0pxR51++uksWrSIjo4OVqxYwdlnn80tt9wCwHvf+15OO+00/v3f/51jjz2Wd7zjHey3335bbeOWW26hpaVl8/S1117LAQccsFNx7Yya9mkmSZIkSdJ4V1pZomtNF30b+kgk+jb00bWmi9JKGzuRJElSg+vpgf7+wWX9/dDbu9ObPuyww1i1ahVLlizZ3GfZgPb2du69917e9a538bvf/Y7W1lbWr1+/1TaOOuooent7N7/qmTADk2aSJEmSJA2rZ20P/RsG32jo39BP77qdv9EgSZIk1VRrKzQ1DS5raoKK2l0746STTuLDH/7woKYZB+y111689a1v5Rvf+AZHHnkkN998c1X2WUsmzSRJkiRJGkbrvq00TRt8o6FpWhMt+1TnRoMkSZJUM8UitLVBczNEZMO2tqy8Cs4++2w+8YlP8LKXvWxQ+Y9//GMef/xxAB577DHuuece9t9//6rss5Zq1qeZJEmSJEkTQXFukbaZbVv1aVacW50bDZIkSVLNFArQ2Zn1Ydbbm9UwKxaz8iqYNWsWH/jAB7YqX758Oe973/uYOnUqmzZt4p3vfCdHHnkkq1atGrTcln2anX/++Zx66qlViW1HREqpbjuvh3nz5qXu7u56hyFJkiRJGkfKm8qUVpboXddLyz4tFOcWKUypzo0GSZIkaXvceeedHHzwwfUOY1wY6r2KiOUppXlDLW9NM0mSJEmSRlCYUqDjoA46DuqodyiSJEmSasQ+zSRJkiRJkiRJkjTpmTSTJEmSJEmSJEnSpGfSTJIkSZIkSZIkSZOeSTNJkiRJkiRJkiRNeibNJEmSJEmSJEmSNOmZNJMkSZIkSZIkSdKoFQoFWlpaNr9WrVpFd3c373//+7d7Wx/5yEc49NBD+chHPlKDSLfP1HoHIEmSJEmSJEmSpNool6FUgp4eaG2FYhEKhZ3b5m677UZvb++gstmzZzNv3rytlt24cSNTp247HfWVr3yF9evXs+uuu+5cUFVgTTNJkiRJkiRJkqQJqFyG9naYPx8WLsyG7e1ZebUtW7aMjo4OAD75yU9yzjnncPzxx3PWWWdRLpf5yEc+wpFHHslhhx3GV77yFQBOOukk+vv7aWtr4+qrr+aPf/wjxxxzDIcddhjHHHMMf/rTnwA4+eSTueKKK4Asyfa2t72t+geANc0kSZIkSZIkSZImpFIJurqgry+b7uvLpkslyPNbO+SJJ56gpaUFgDlz5vCd73xnq2WWL1/OT3/6U3bbbTcuueQS9thjD375y1/y1FNP8epXv5rjjz+e6667jubm5s211k488UTOOussFixYwKWXXsr73/9+vvvd73LJJZfw6le/mjlz5vD5z3+e2267bceDH4ZJM0mSJEmSJEmSpAmopwf6+weX9fdDb+/OJc2Gap5xSyeddBK77bYbANdffz0rVqzgmmuuAeCRRx7h7rvvZs6cOYPWufXWW/n2t78NwJlnnslHP/pRAJ7//OezaNEiXve61/Gd73yHvfbaa8eDH4ZJM0nSxFKLRpolSZIkSZKkcai1FZqanqlpBtl0XkmsppqamjaPp5T44he/SHt7+3ZtIyI2j99+++3svffe3H///VWLcUv2aSZJmjjGspFmSZIkSZIkqcEVi9DWBs3NEJEN29qy8rHU3t7OxRdfzNNPPw3AXXfdRf+WVeCAV73qVXzzm98E4KqrruKv//qvAfjFL35BqVSip6eHz33uc/zhD3+oSZzWNJMkTRy1aqRZkiRJkiRJGocKBejszG6P9fZmNczq0TDTO9/5TlatWsXLX/5yUkrMmDGD7373u1std9FFF3H22Wfz2c9+lhkzZnDZZZfx1FNP8a53vYvLLruM/fbbj89//vOcffbZ/PjHPx5UE60aIqVU1Q02unnz5qXu7u56hyFJqoXFi7MaZpX/2yJg0SI4//z6xSVJkiRJkiRVyZ133snBBx9c7zDGhaHeq4hYnlKaN9TyNs8oSZo4BhpprjRWjTRLkiRJkiRJGtdMmknSKJQ3lVl611IW37SYpXctpbzJPrIaUqM00ixJkiRJkiRp3LFPM0kaQXlTmfYr2+la00X/hn6apjXRNrONzjM6KUwZ48Z/NbxGaaRZkiRJkiRJ0rhj0kySRlBaWaJrTRd9G/oA6NvQR9eaLkorS3Qc1FHn6LSVQgE6OrKXJEmSJEmSJI2SzTNK0gh61vbQv6F/UFn/hn561/XWKSJJkiRJkiRJUrWZNJOkEbTu20rTtKZBZU3TmmjZp6VOEUmSJEmSJEmSqs2kmSSNoDi3SNvMNpqnNRMEzdOaaZvZRnFusd6hSZIkSZIkSdKYW7duHaeffjoHHHAAhxxyCCeccAKXXHIJHeO8yxT7NJOkERSmFOg8o5PSyhK963pp2aeF4twihSmFeocmSZIkSZIkScMqbypTWlmiZ20Prfu27vS9zZQSb3rTm1iwYAHf/OY3Aejt7eX73//+6OIplykUGvPeqkkzSRqFwpQCHQd10HHQ+H5SQlLtVftCVJIkSZIkaUeVN5Vpv7KdrjVd9G/op2laE20z2+g8o3OH71f85Cc/YZddduHcc8/dXNbS0sJf/vIXbrzxRk499VTuuOMOjjjiCK688koigtmzZ3P22Wdz/fXX8773vY+XvOQlnHvuuTz++OMccMABXHrppey5554cffTRtLa2snz5ctavX88VV1zBhRdeyO23385pp53GBRdcAMCVV17JRRddxIYNG2hra+PLX/5yVRJxNWueMSIujYgHI+KOirKrI6I3f62KiN68fHZEPFEx7z8q1jkiIm6PiJURcVFERF6+V0TcEBF358M9a3UskiRJozFwITr/2vksXLaQ+dfOp/3KdsqbyvUOTZIkSZIkTUKllSW61nTRt6GPRKJvQx9da7oorSzt8DYHEmJD6enp4Qtf+AK//e1vuffee/nZz362ed706dP56U9/yumnn85ZZ53FZz7zGVasWMHLXvYyPvWpT21ebtq0adx8882ce+65nHzyyXzpS1/ijjvu4PLLL+ehhx7izjvv5Oqrr+ZnP/sZvb29FAoFrrrqqh0+nkq17NPscuANlQUppdNSSi0ppRbgWuDbFbPvGZiXUjq3ovxi4BzgwPw1sM2PAzemlA4EbsynJUmS6qYWF6KSJEmSJEk7qmdtD/0b+geV9W/op3ddb03294pXvIJZs2YxZcoUWlpaWLVq1eZ5p512GgCPPPIIf/nLX3jta18LwIIFC7j55ps3L3fSSScB8LKXvYxDDz2Ufffdl1133ZUXvehF3Hfffdx4440sX76cI488kpaWFm688UbuvffeqsRfs+YZU0o3R8TsoebltcXeArx+uG1ExL7A7imlW/PpK4BTgBJwMnB0vujXgWXAx3Y+cknSeFYuQ6kEPT3Q2grFIjRoE8magIa7ELV5V0mSJEmSNNZa922laVoTfRv6Npc1TWuiZZ+WHd7moYceyjXXXDPkvF133XXzeKFQYOPGjc/st6lpVNsf2MaUKVMGbW/KlCls3LiRlBILFizgwgsv3JHwh1XLmmbDOQp4IKV0d0XZnIjoiYibIuKovGwmsLpimdV5GcDzU0prAfLh82odtCSpsZXL0N4O8+fDwoXZsL09K5fGwsCFaKUdvRAtbyqz9K6lLL5pMUvvWmoTj5IkSZIkabsV5xZpm9lG87RmgqB5WjNtM9sozi3u8DZf//rX89RTT/HVr351c9kvf/lLbrrpplGtv8cee7Dnnntyyy23APCNb3xjc62z0TjmmGO45pprePDBBwF4+OGH+eMf/7gdR7BtNatpNoL5wJKK6bXA/imlhyLiCOC7EXEoEEOsm7Z3ZxFxDlkTj+y///47EK4kaTwolaCrC/ryB2f6+rLpUgk6rOSjMTBwIbpl57rbeyFai056JUmSJEnS5FOYUqDzjE5KK0v0ruulZZ8WinOLO3V/ISL4zne+wwc/+EE+/elPM336dGbPns0pp5wy6m18/etf59xzz+Xxxx/nRS96EZdddtmo1z3kkEO44IILOP7449m0aRO77LILX/rSl3jhC1+4I4czSKS03Tmo0W88a55xaUrppRVlU4E1wBEppdXbWG8Z8OF8uZ+klF6Sl88Hjk4p/X1E/D4fX5s347gspfTikWKaN29e6u7u3rkDkyQ1pMWLsxpmlf/aImDRIjj//PrFpcmlvKm80xeiS+9ayvxr5w9qOqF5WjNL3rzEZh4lSZIkSZrk7rzzTg4++OB6hzEuDPVeRcTylNK8oZavR/OMxwK/q0yYRcSMiCjk4y8CDgTuzZtdfCwiXpn3g3YW8L18teuABfn4gopySdIk1doKWzaN3NQELTveRLO03QpTCnQc1MH5rzmfjoM6dujJrbHupFeSJEmSJEk1TJpFxBLgVuDFEbE6Iv4un3U6g5tmBHgNsCIifg1cA5ybUno4n/du4D+BlcA9QCkv/zRwXETcDRyXT0uSJrFiEdraoLk5q2HW3JxNF3e8iWapLqrZN5okSZIkSZJGp2Z9mqWU5m+j/O1DlF0LXLuN5buBlw5R/hBwzM5FKUmaSAoF6OzM+jDr7c1qmBWLWbk0nlSrbzRJkiRJkiSNXs2SZpIk1UOhAB0d2Usar2rRSa8kSZIkSZo4UkpkvVppW1JK272OSTNJkqQGNNA3WsdBZoAlSZIkSdIzpk+fzkMPPcTee+9t4mwbUko89NBDTJ8+fbvWM2kmSZIkSZIkSZI0TsyaNYvVq1ezfv36eofS0KZPn86sWbO2ax2TZpIkSZIkSZIkSePELrvswpw5c+odxoQ0pd4BSJIkSZIkSZIkSfVm0kySJEmSJEmSJEmTnkkzSZIkSZIkSZIkTXomzSRJkiRJkiRJkjTpmTSTJEmSJEmSJEnSpGfSTJIkSZIkSZIkSZOeSTNJkiRJkiRJkiRNelPrHYAkSZIkSVJVlctQKkFPD7S2QrEIhUK9o5IkSVKDM2kmSZIkSZImjnIZ2tuhqwv6+6GpCdraoLPTxJkkSZKGZfOMkiRJkiRp4iiVsoRZXx+klA27urJySZIkaRgmzSRJkiRJ0sTR05PVMKvU3w+9vfWJR5IkSeOGzTNKkiRJkqSJo7U1a5Kxr++ZsqYmaGmpX0yqObuxkyRJ1WDSTJIk1U15U5nSyhI9a3to3beV4twihSne3ZAkSTuhWMz6MNuyT7Nisd6RqUbsxk6SJFWLSTNJklQX5U1l2q9sp2tNF/0b+mma1kTbzDY6z+g0cSZJknZcoZBlS0qlrEnGlharHU1wld3YweBu7Do66hubJEkaX+zTTJIk1UVpZYmuNV30begjkejb0EfXmi5KK0v1Dk2SJI13hUKWLTn//GxowmxCsxs7SZJULSbNJElSXfSs7aF/w+C7G/0b+uld590NSZIkjd5AN3aV7MZOkiTtCJNmkiSpLlr3baVp2uC7G03TmmjZx7sbkiRJGr2BbuyamyEiG9qNnSRJ2hH2aSZJkuqiOLdI28y2rfo0K8717oYkSZJGz27sJElStURKqd4xjKl58+al7u7ueochSZKA8qYypZUletf10rJPC8W5RQpTvLshSZIkSZKk2oiI5SmleUPNs6aZJEmqm8KUAh0HddBxUEe9Q5EkSZIkSdIkZ59mkiRJkiRJkiRJmvRMmkmSJEmSJEmSJGnSM2kmSZIkSZIkSZKkSc+kmSRJkiRJkiRJkiY9k2aSJEmSJEmSJEma9GqWNIuISyPiwYi4o6LskxGxJiJ689cJFfPOi4iVEfH7iGivKH9DXrYyIj5eUT4nIroi4u6IuDoiptXqWCRJkiRJkiRJkjSx1bKm2eXAG4Yo/7eUUkv++iFARBwCnA4cmq/z5YgoREQB+BJQBA4B5ufLAnwm39aBwP8Af1fDY5EkSZIkSZIkSdIEVrOkWUrpZuDhUS5+MvDNlNJTKaU/ACuBV+SvlSmle1NKG4BvAidHRACvB67J1/86cEpVD0CSJEmSJEmSJEmTRj36NHtfRKzIm2/cMy+bCdxXsczqvGxb5XsDf0kpbdyiXJIkSZIkSZIkSdpuY500uxg4AGgB1gKfz8tjiGXTDpQPKSLOiYjuiOhev3799kUsSZIkSZIkSZKkCW9Mk2YppQdSSuWU0ibgq2TNL0JWU+wFFYvOAu4fpvzPwHMiYuoW5dva7yUppXkppXkzZsyozsFIkiRJkiRJkiRpwhjTpFlE7Fsx+Sbgjnz8OuD0iNg1IuYABwK/AH4JHBgRcyJiGnA6cF1KKQE/AU7N118AfG8sjkGSJEmSJEmSJEkTz9SRF9kxEbEEOBp4bkSsBhYCR0dEC1lTiquAvwdIKf0mIr4F/BbYCLw3pVTOt/M+oBMoAJemlH6T7+JjwDcj4gKgB/harY5FkiRJkiRJkiRJE1tklbYmj3nz5qXu7u56hyFJkiRJkiRJkqQxFhHLU0rzhpo3ps0zSpIkSZIkSZIkSY3IpJkkSZIkSZIkSZImPZNmkiRJkiRJkiRJmvRMmkmSJEmSJEmSJGnSM2kmSZIkSZIkSZKkSW9qvQNQgyqXoVSCnh5obYViEQqFekclSZIkaTzw94QkSZKkccikmbZWLkN7O3R1QX8/NDVBWxt0dvpDV5IkSdLw/D0hSZIkaZyyeUZtrVTKfuD29UFK2bCrKyuXJEmSpOH4e0KSJEnSOGXSTFvr6cmeCK3U3w+9vfWJR5IkSdL44e8JSZIkSeOUSTNtrbU1a0KlUlMTtLTUJx5JkiRJ44e/JyRJkiSNU6OC6cYAACAASURBVCbNtLViMetzoLkZIrJhW1tWLkmSJEnD8feEJEmSpHFqar0DUAMqFLJOukulrAmVlpbsB66ddkuSJEkaib8nJEmSJI1TkVKqdwxjat68eam7u7veYUiSJEmSJEmSJGmMRcTylNK8oebZPKMkSZIkSZIkSZImPZNmkiRJkiRJkiRJmvRMmkmSJEmSJEmSJGnSM2kmSZIkSZIkSZKkSc+kmSRJkiRJkiRJkiY9k2aSJEmSJEmSJEma9EyaSZIkSZIkSZIkadIzaSZJkiRJkiRJkqRJb8SkWUQcEBG75uNHR8T7I+I5tQ9NkiRJkiRJkiRJGhujqWl2LVCOiLnA14A5wH/VNCpJkiRJkiRJkiRpDI0mabYppbQReBPwhZTSPwL71jYsSZIkSZIkSZIkaeyMJmn2dETMBxYAS/OyXWoXkiRJkiRJkiRJkjS2RpM0ewfwV8C/pJT+EBFzgCtrG5YkSZIkSZIkSZI0dqaOtEBK6bcR8TFg/3z6D8Cnax2YJEmSJEmSJEmSNFZGrGkWEScCvcB/59MtEXFdrQOTJEmSJEmSJEmSxspommf8JPAK4C8AKaVeYE4NY5IkSZIkSZIkSZLG1GiSZhtTSo9sUZZGWikiLo2IByPijoqyz0bE7yJiRUR8JyKek5fPjognIqI3f/1HxTpHRMTtEbEyIi6KiMjL94qIGyLi7ny45+gOWZIkSZIkSZIkSRpsNEmzOyLirUAhIg6MiC8CPx/FepcDb9ii7AbgpSmlw4C7gPMq5t2TUmrJX+dWlF8MnAMcmL8Gtvlx4MaU0oHAjfm0JEmSJEmSJEmStN1GkzT7B+BQ4Cngv4BHgA+MtFJK6Wbg4S3Krk8pbcwnbwNmDbeNiNgX2D2ldGtKKQFXAKfks08Gvp6Pf72iXJIkSZIkSZIkSdouo0mavTGl9L9TSkfmr/OBk6qw77OBUsX0nIjoiYibIuKovGwmsLpimdV5GcDzU0prAfLh87a1o4g4JyK6I6J7/fr1VQhdkiRJkiRJkiRJE8lokmbnjbJs1CLifwMbgavyorXA/imlVuB/Af8VEbsDMcTqI/anttUKKV2SUpqXUpo3Y8aMHQ1bkiRJkiRJkiRJE9TUbc2IiCJwAjAzIi6qmLU7WcJrh0TEAqADOCZvcpGU0lNkzT+SUloeEfcAB5HVLKtswnEWcH8+/kBE7JtSWps34/jgjsYkSZIkSZIkSZKkyW24mmb3A93Ak8Dyitd1QPuO7Cwi3gB8DDgppfR4RfmMiCjk4y8CDgTuzZtdfCwiXhkRAZwFfC9f7TpgQT6+oKJckiRJkiRJkiRJ2i7brGmWUvo18OuIeBj4QUpp0/ZsOCKWAEcDz42I1cBCsmYddwVuyHJg3JZSOhd4DbAoIjYCZeDclNLD+abeDVwO7EbWB9pAP2ifBr4VEX8H/An42+2JT5IkSZIkSZIkSRoQeQuJ214g4krgr4BrgctSSneORWC1Mm/evNTd3V3vMCRJkiRJkiRJkjTGImJ5SmneUPOGa54RgJTSGUArcA9wWUTcGhHnRMSzqxynJEmSJEmSJEmSVBcjJs0AUkqPktU0+yawL/Am4FcR8Q81jE2SJEmSJEmSJEkaEyMmzSLixIj4DvBjYBfgFSmlInA48OEaxydJkiRJkiRJkiTV3NRRLPO3wL+llG6uLEwpPR4RZ9cmLEmSJEmSJEmSJGnsjJg0SymdNTAeEc8FHkoppXzejTWMTZIkSZIkSZIkSRoT22yeMSJeGRHLIuLbEdEaEXcAdwAPRMQbxi5ESZIkSZIkSZIkqbaGq2n278A/AXuQ9WdWTCndFhEvAZYA/z0G8UmSJEmSJEmSJEk1t82aZsDUlNL1KaX/B6xLKd0GkFL63diEJkmSJEmSJEmSJI2N4ZJmmyrGn9hiXqpBLJIkSZIkSZIkSVJdDNc84+ER8SgQwG75OPn09JpHJkmSJEmSJEmSJI2RbSbNUkqFsQxEkiRJkiRJkiRJqpfhmmeUJEmSJEmSJEmSJgWTZpIkSZIkSZIkSZr0TJpJkiRJkiRJkiRp0jNpJkmSJEmSJEmSpElvxKRZRPxNRNwdEY9ExKMR8VhEPDoWwUmSJEmSJEmSJEljYeoolvk/wIkppTtrHYwkSZIkSZIkSZJUD6NpnvEBE2aSJEmSJEmSJEmayEZT06w7Iq4Gvgs8NVCYUvp2zaKSJEmSJEmSJEmSxtBokma7A48Dx1eUJcCkmSRJkiRJkiRJkiaEEZNmKaV3jEUgkiRJkiRJkiRJUr1sM2kWER9NKf2fiPgiWc2yQVJK769pZJIkSZIkSZIkSdIYGa6m2Z35sHssApEkSZIkSZIkSZLqZZtJs5TS9/Ph18cuHEmSJEmSJEmSJGnsTal3AJIkSZIkSZIkSVK9mTSTJEmSJEmSJEnSpGfSTJIkSZIkSZIkSZPeiEmziDgoIm6MiDvy6cMi4vzahyZJkiRJkiRJkiSNjdHUNPsqcB7wNEBKaQVwei2DkiRJkiRJkiRJY6tchqVLYfHibFgu1zsiaWyNJmn2rJTSL7Yo2ziajUfEpRHx4EAttbxsr4i4ISLuzod75uURERdFxMqIWBERL69YZ0G+/N0RsaCi/IiIuD1f56KIiNHEJUmSJEmSJEmSnlEuQ3s7zJ8PCxdmw/Z2E2eaXEaTNPtzRBwAJICIOBVYO8rtXw68YYuyjwM3ppQOBG7MpwGKwIH56xzg4nx/ewELgTbgFcDCgURbvsw5FettuS9Jaig+rSNJkiRJkqRGVCpBVxf09UFK2bCrKytXxnt7E9/UUSzzXuAS4CURsQb4A3DGaDaeUro5ImZvUXwycHQ+/nVgGfCxvPyKlFICbouI50TEvvmyN6SUHgaIiBuAN0TEMmD3lNKtefkVwCnA8H/Cv/89HH304LK3vAXe8x54/HE44YSt13n727PXn/8Mp5669fx3vxtOOw3uuw/OPHPr+R/6EJx4Yrbvv//7reeffz4ceyz09sIHP7j1/H/9V3jVq+DnP4d/+qet53/hC9DSAj/6EVxwwdbzv/IVePGL4fvfh89/fuv53/gGvOAFcPXVcPHFW8+/5hp47nPh8suz15Z++EN41rPgy1+Gb31r6/nLlmXDz30u+yaptNtuz3zrLl4MN944eP7ee8O112bj550Ht946eP6sWXDlldn4Bz+YvYeVDjoILrkkGz/nHMp3/Z7S3g/T09xHa18zxRe8jsIXLsrmn3EGrF49eP2/+iu48MJs/M1vhoceAqBMyrZz+PNoPf0fKc4tUnhjBzzxxOD1Ozrgwx/Oxrc876Aq51751NMoXbGenkXX0dq8kuJeXRRiUzbfcy8bNsC5x113UU5TaF/xWboePZj+TdNpap5CWxt0Pu9MCvffN3j9bZx7mx1zDPzzP2fjxWJdzj2/98bPuTdIS0v2/sF2fe9t5rnnuQeee+Pg3CunKZQebqOnb252ffD991CY7bnnuTf+vvfKS79P6f6b6PneV2i9bRXFh/aiQEUDI5572bjn3tbz/Z+bjXvubT3fcy8b99zber7nnuceTPpzr+cf/kh/3zuorGvT35/o7Q06kude+fcrB9/b22UDba+ZTmcnFBZ47m2l0b/3tmHEpFlK6V7g2IhoAqaklB4b9daH9vyU0tp822sj4nl5+Uyg8q7x6rxsuPLVQ5RvJSLOIauRxmG77rqT4Wu8KrOJ9sNX0LX7o/QXNtFUnkJbeoTOTf9GYUphO7aTntlOuoOma39B28w2OpnK6LdCdjPr9hfSsxhaD5lCMU15Jtk12m1sCtrboevWvel//B00TXmStt3vpPOwj2z3tlR7pYfb6Hr0YPo2PQuoeFrniMPpGPQ1J0nSjtvqIY0pT9J2xhQ6b2K7rlWkeiuTaP9/J9G1rpv+DX00HTKFtkd3p/PXhw1OnEmStBM2P2x07SG0NkHxpV4zafJqbV5J05QnN9+7AmjaLdHSEnk7dJPbVvf2np6+uSZeR51jU/VEVrFrmAUiysBngfPyWmBExK9SSi8fdsVn1p8NLE0pvTSf/ktK6TkV8/8npbRnRPwAuDCl9NO8/Ebgo8DrgV1TShfk5f8MPA7cnC9/bF5+FPDRlNKJw8Uzb9681N3dPZrQNcEsvWsp86+dT9+Gvs1lzdOaWfLmJXQcNPqvtWpsZ6B94K4u6O+HpiayGkedUNiOK7OlS7O2hfueCYXmZliyJHsQQVDeVKa0skTP2h5a923NagVuR5K0mhYvztqDrvzajYBFi7IHMyRJqgavDzRRVOv6XZKkbanW/RlpovBvYnje25s4ImJ5SmneUPNG06fZb/Llrs/7FwN26rG+B/JmF8mHD+blq4EXVCw3C7h/hPJZQ5RLQ+pZ20P/hv5BZf0b+uld17uNNWq3nWq1D9zTk/0DGxRL/9Y1iSer8qYy7Ve2M//a+SxctpD5186n/cp2ypvq09hwa2t2sVGpqSmrhSxJUrV4faCJolrX7xOWHWpI0k6z/yZpsEIhS5AtWZIlgpYsMWFWyXt7k8NokmYbU0ofBb4K3BIRR7BzlTGvAxbk4wuA71WUnxWZVwKP5M04dgLHR8SeEbEncDzQmc97LCJeGREBnFWxLWkrrfu20jRt8Lda07QmWvbZvm+1amynWjez/KIeXmllia41XfRt6COR6NvQR9eaLkor63P1WyxmT+c0N2dPoTQ3Z9PFYl3CkSRNUF4faKKo1vX7hDTwGPj8+dnjzvPnZ9MmziRpu/iwkbS1QiFroeL887OhCbNneG9vchhN0iwAUkrfAt4CXAa8aDQbj4glwK3AiyNidUT8HfBp4LiIuBs4Lp8G+CFwL7CSLEH3nny/DwOLgV/mr0V5GcC7gf/M17kH8DkQbVNxbpG2mW00T2smCJqnNdM2s43i3O37VqvGdqp1M8sv6uE12tPJPq0jSRoLXh9ooqjW9fuEZNUISaoKHzaStD28tzc5jKZPsyNSSssrpncHTkkpXVHr4GrBPs0mt4H+rXrX9dKyT8sO92+1s9upZvvA5XL227i3N7uoKxb9oh5gPxiSpMnK6wNNFNW6fp9w7FBDqpmB/6E9PVlCxf+hE5v9N0nS5DRcn2bbTJpFxOtTSj+OiL8Zan5K6dtVjHHMmDRTo/BmVu0N9GnWtaaL/g39NE1rom1mG51ndHqzRZKkccabmJowqnEyL12aNcnY98zDYTQ3Z487d/hwmLSjTKBMTt6fmZy8tpQmtx1Nmn0qpbQwIi4bYnZKKZ1dzSDHikkzaXLx6WSpNvyBIWkseRNTDaEa//yqdTKXy5SPL1L6+R70PHkwrdPvpPiqRyhcX/KPQtoJ5qOlycFrS0nDJc2mbmullNLCfPiOWgUmSbVWmFKg46AOm2OUqsgfGJLGWmX3TTC4+yZvYmpMVOufX5VO5jIF2umkizL9TKGJTbRRoJPAf8XSjuvpyf7EK/X3ZzWQ/H8jTRxeW0oazpRtzYiIEyPihRXTn4iIX0fEdRExZ2zCkyQNqVzOHoNcvDgblsv1jkjbMgE/q8ofGCkN/oEhSbUw3E1MaUxU659flU7mUgm6fhH0PTmVxBT6npxK1y/C/8XSTmptzXLilZqasib7JE0cXltKGs42a5oB/wK8EiAiOoAzgPlAK/AfQHvNo5Mkbc1qPuPHBP2sfAJX0lgbuIlZ2VyWNzE1pqr1z69KJ7P/i6XaKBazy/UtL9+LxXpHJqmaGvHa0i4QpMaxzZpmZP2WPZ6P/w3wtZTS8pTSfwIzah+aJGlIVvMZPyboZ1XNJ3AnYEU8STUwcBOzuRkisqE3MTWmqvXPr0ons7VhpNooFLLn25YsgUWLsuE4f95N0hAa7dpy4Hnb+fNh4cJs2N7u72OpXoaraRYR0Qw8DhwDfLli3vSaRiVp7PlIy/jho8XjxwT9rKr1BO4ErYgnTRiNdGkwcBOzVMq+QltavFSZLBrmPKzWP78qnczWhpFqp1DILtXH8eW6pBE02rWlfaxJjWW4pNkXgF7gUeDOlFI3QES0AmvHIDZJY8U71+NLI7YjoKFN0M+qWj8wJuoPg4a5watxqbypTGlliZ61PbTu20pxbpHClLE/gRrx0sCbmJNPQ52H1by7VoWTudFu9kmSNN400rXlBH3eVhq3tpk0SyldGhGdwPOAX1fMWge8o9aBSRpDE/XO9UTlo8XjxwT+rAqU6aBER+oh6+60CGzfnbpq/jBolERVQ93g1bhT3lSm/cp2utZ00b+hn6ZpTbTNbKPzjM4xT5x5aaBG0HDnYSPdXaPhwpEkSTtogj5vK41bw9U0I6W0BlizRZm1zKSJxkdaxhcfLR4/JupnVaXMULV+GDRSoqrhbvBqXCmtLNG1pou+DdkJ1Lehj641XZRWlug4aGxPIC8N1Ag8DyVJ0mQwgZ+3lcalYZNmkmqnUZpfAnykZTzy0eLxYyJ+VlXKDFXrh0EjJaq8waud0bO2h/4Ng0+g/g399K7rHfOkmZcGagSeh5IkaTKYqM/bSuOVSTOpDhqp+SXAR1rUGBqlfT2NrEqZoWr9MGikRJU3eLUzWvdtpWla0+aaZgBN05po2WcHTqCd/E710kCNwPNwfGmohwIlSRpnJuLzttJ4NWLSLCK+kVI6c6QySaPXSM0vAT7SovprpPb1NLIqZoaq8cOgkRJV3uAdO1W7OdtACfvi3CJtM9u2eqimOHc7T6AqfKd6aaBGUK3z0GRO7TXcQ4GSJEnSDoqU0vALRPwqpfTyiukCcHtK6ZBaB1cL8+bNS93d3fUOQ5Pc4psWs3DZQhLP/P0FwaLXLeL815xfx8ikOlm6FObPH5z1aG6GJUt8zKoRNViSs8HC2ZyDMdFQO1W7OdtoJw/P3NzvXddLyz4tO3Zz3+9UaTOTOWNj6V1LmX/t/EE1ZZunNbPkzUvq81CgpJppoOeNJEnaYRGxPKU0b6h526xpFhHnAf8E7BYRjw4UAxuAS6oepTSJVLX5pSqp1oWvF9DjS8N8Xj09lPueoMQb6aGVVnoo9v03BTuCakwNVgWlwcKxWY0xULUa243UIV6uMKVAx0EdO3eTuZHaLNW40zDXBlXScC08TFCN1CejpNppwOeNJEmqum0mzVJKFwIXRsSFKaXzxjAmacKrWvNLVVKtC18voMeXRvq8yoe10l64ga7yPPp5Fk08Tluhm86X9eOp06AaLDPUYOGoxqp2c3aiJpcaqc1SjSuNdG1QLSZzxkYjPhQoqfoa8HkjSZKqbsoollkaEU0AEXFGRPzfiHhhjeOSJrTClAKdZ3Sy5M1LWPS6RSx585K6NhFTeeGb0uAL33psZyIrbyqz9K6lLL5pMUvvWkp5U7lusWSfVxr8ed2W6vJ5lSjSRRt9PJtEgT6eTRdtlLAjKElbG7g5W2mHbs4OJJcGbWgCJJcGOtdrboaIbGjnehqFiXgtV7XvCw1r4KHA5mnNBEHztOa6PhQoqTaGe95IkqSJYps1zSpcDBweEYcDHwW+BlwBvLaWgUkTXVWaX6qSaj1oP1Ef2Aeq0lZRo/Wp0bN8E/19kLW8m+nvT/T+KtHRMZpnKqoYy4oC/Zt2G1TWv2k3em8POk4e01AkjQNVq7E9kFzaslrNeE8uNVqbpRo3JuK1XKO18DBRDTwUuNN9MkpqaFZmlyRNBqNJmm1MKaWIOBn4/1JKX4uIBbUOTGpoE6yzh2pd+E7YC+gqtVXUaH1qtJa7aeIQ+mjeXNbE47Rs/C3wirGNpRWammKLcyfG/7kjqSaqdnN2IieXbLNUO2AiXsuZzBk7jfRQoKTamKjPG0mSVGk0SbPHIuI84EzgqIgoALvUNiypgU3Azh6qdeE7YS+gq9Rwe6P1qVEsXE8bj9JF2zP9iNFFcWoXY500m7DnjqSaqdrNWZNL0mYT9f+xyRxJqo6J/LyRJEkDRpM0Ow14K3B2SmldROwPfLa2YUkNbAL2fFutC9+qXkA3Um2+nh7KfU9Q4o300EorPRT7/pvCdrZV1GgdpBeOaKGz6c2U+o+ilxZa6KXYdAuFl1819rH440uSpLrz/7EkaSQ+b/T/t3f/UZKddZ3HP9+u0CapCgQ0OMMkgeBkFPxBN5ZpBI0gkqawNbIBTWsQAQ26sOCuugs4x8FpWXFFfuxZDmsIeJBgByQgY0vZCQohLlCkJ9UCIRIbiDCTGYiCkKqYdFL93T/uraSqp7u6q/tW3VvPfb/OmVNTt6qrnvp173Of7/P9PgCA0Jm7b30ns8dKutDdP2JmZ0oquPvdA2/dAJTLZV9aWkq7GRhlc3PSoUPR6uhtZtLhw9LBg+m1KyQZy+ZrfWhB05cVVWuVH8rIKixp8bqmCpdu/0wha2uaZe19BgAAAAAAAIBBM7Oj7l7e6LYtM83M7NckXSnpUZK+R9I+Sf9X0jOTbCQwMkJc7CFrMpbNV1VFNd2nhs6MmqOzVNOUqvoO9dOazK2pwXRyAAAAAAAAAHjQdsozvkzR4jY1SXL3fzazRw+0VUCWhbrYQ5bU69F726nZjAI7KQTN6p8pqLl2Rndz1s7Q8mdNM5f291iZW1OD2hoAgBHRWmupulJV/URdk3sn0514AgAAAAAI0naCZve5+6qZSZLM7DRJW9d0BEJFds7gZSybL2qOrWuOkVwIAMCQZK7EMQAAAADsQqsVDS/X69HYI8PL2bGdoNmNZvYaSWeY2bMk/WdJfz3YZgEZR3bOYGUsmy9jzQEAIHeqK1XVjtfUWI1msDRWG6odr6m6Us1O9jYAAAAAbEOrJU1PnzrWuLhI4CwLthM0e5Wkl0j6rKSXSvqwpKsH2SgAOZexbL6MNQcA+sP0NQSgfqKu5mp36ebmalPLJ5cJmgEAAAAYKdVqFDBrV7VqNKLr1So5GlmwZdDM3dckvT3+BwDDkbFsvow1BwC2h+lrCMTk3kkVx4sPZppJUnG8qIk91EpGfrHOHwAAwGiq16NT9E7NZjRZn7HH9G0ZNDOzL2uDNczc/fEDaREAABgYko5yhulrCERlf0VT+6ZOWdOssp9aycgn1vkDAAAYXZOT0ZzWxkNzAlUsRtWtkL7tlGcsd/z/dEnPl/SonT6hmX2vpPd2bHq8pN+TdLakX5N0V7z9Ne7+4fhvXq2oRGRL0ivcfTHe/mxJb5FUkHS1u79+p+0CACB0JB3lENPXEIjCWEGLVyyqulLV8sllTeyZIKsGucY6f1sjEw8AAGRVpRKNx6wfn6kwJzATtlOe8d/WbXqzmf2DokBX39z9C5ImJMnMCpKOS/qgpBdJepO7v6Hz/mb2REmXS/p+SY+R9BEzOxDf/FZJz5J0TNLNZnbE3T+/k3YBABA6ko62IbRUPKavISCFsYJmDswQEADEOn9bIRMPAABkWaEQTWCuVqM5rRMToz/8EJLtlGd8csfVMUWZZ2cl9PzPlPRFd/8XM9vsPpdKutbd75P0ZTNbkXRRfNuKu38pbue18X0JmgEAsAGSjrYQYioe09cAILwJEWKdv62QiQcAALKuUIjGYhiPyZ7tlGf8k47/PyDpDkk/n9DzXy5pvuP6y83slyUtSfotd/+mpH2SPtVxn2PxNkn66rrtUxs9iZldKelKSTr//POTaTkAIJMoxbM5ko62EGIqHtPXsAvsTxGEVkutSyqqfuIRqt/7BE2e/i5Vnvq/Vbi+OtL7Qtb5641MPAAAAOzUdsozPmMQT2xm45J+VtKr401vkzQnyePLP5H0YkkbpaC5oqy3jbafutH9KklXSVK5XN7wPgCA0Ucpnt5IOtpCqKl4TF/DDrA/RShaC1VN3/hq1VplNXWmivfeo6kbl7S4UFXh0tHdL7LOX29k4gEAAGCntlOe8b/1ut3d37jD565IusXdvxY/ztc6nvPtkhbiq8ckndfxd+dKujP+/2bbAQDbEFoWQcileJKoLEXS0RZIxQMexP4Uoaj+ZUO11k+oEa8w0NBZqrXKqr7/bzRzacqN2yXW+dscmXjIAo43AACMpu2UZyxL+hFJR+LrPyPp4+oujbgTs+oozWhme939RHz1uZI+F///iKS/MLM3SnqMpAslfVpRBtqFZnaBpOOKSj3+4i7bBAC5EWIWQaileJJcaoukox5IxQMexP4UoahrUk2d2bWtqTO1rAmN7jcZWyETD2njeAMAwOjaTtDsuyQ92d3vliQze62kv3T3X93pk5rZmZKeJemlHZv/l5lNKCqxeEf7Nne/1czeJ+nzitZUe5m7t+LHebmkRUkFSe9091t32iYAyJsQswhCLcUT4lJbmUQqHvAg9qcIxeTz96t47X1qtB4KnBUL92nieRem2CoMA5l4SBPHG4SErEkAebOdoNn5klY7rq9KetxuntTd75H0neu2vaDH/V8n6XUbbP+wpA/vpi0AkFchZhGEWoon1KW2MolUvIHjpHs0sD9FKCozBU39xBmqfeIBNe8dU/H0NU099QxVZjZaOhtAKgLsHHC8QSjImgSQR9sJmr1b0qfN7IOKssCeK+nPB9oqYJ3Q1l0CsiCpLIIs/T5DLcUT9FJbAQ6SYHOcdG8tKz8J9qcIRaEgLV5vqlZPi5OIxzjUAFkSaOeA4w1CQdYkgDwyd9/6TmZPlvTj8dWPu3t9oK0aoHK57EtLS2k3IzeSGPgJcd0lIAuS+G3x+xyOQMcSAn5h2MzCgjQ72z2AVCpJ8/OcdEv8JIaB9xgAMibQzgHHG4Ribk46dEjqHD42kw4flg4eTK9dALBbZnbU3csb3badTDNJOlPSt939z8zsHDO7wN2/nFwTEaKkOokhrrsEZEESWQT8Pocj2KW2mLaYO5Qq6i3Un0SmMpJD3Z8CwKgKtHPA8Qa7kZXKAxJZkwDyacugmZkdklSW9L2S/kzSwyRdI+lpg20aRl1SAz8hrrsEZMVuF0jn9zk8QS61FeggCTbHSXdvIf4kspiRXFBLM6pqxuuSJiVVJDGKCQCpCLhzEGT/HQOXtSzFSiV6Vkea0AAAIABJREFU/vXtqYz2MrcA0NPYNu7zXEk/K6kpSe5+p6SzBtkohKHXwE8/2usuddrJuksAksfvE7vSHiTpFMggCTbWPukulaKyLqUSJ92dQvxJdGYku7wrIzkV7ZGo2dmo1tDsbHS91UqnPXGTFhai8kcLC6k2BQCGj84B0KVzArp79wT0NLSzJufno5KM8/OUGQUQvu2UZ1x1dzczlyQzK271B4CU3ISxyv6KpvZNnTJDubKfTjSQNn6f2BWmLeYOpYp6C/EnkbmM5IzVwMzabHIAGDo6B0CXLFYeIGsSQN5sJ2j2PjP7U0lnm9mvSXqxpKsH2yyEIKmBnyTWXUKOZakYeID4fWJXGCTJJU66NxfiT6Kdkdxe+1JKOSM5YyNRGYvhAUA66BwADwq4YmliGObJHz5zDJu5+9Z3MnuWpEskmaRFd79h0A0blHK57EtLS2k3IzfaO7VQBn4wYpi+DQBAqjK3ptnCQlSSsXMkqlSKag2lMFg7NxdView8JTOLyh8dPDj05gAAgJQxjNEb70/+8JljUMzsqLuXN7xtO0GzdQ9WkHS5u78nicYNG0EzIEcyNjAGAEAetdZa2clIzthZN10VAMCwkKkxOpiAvjn6TvnDZ45B6RU027Q8o5k9XNLLJO2TdETSDfH135G0LGkkg2YAciRjJZgAAGLEJocKYwXNHJhJZw2zUxqTrRqYIa5jBwDInozNGcEWqFi6OYZ58ofPHGnotabZuyV9U9InJf2qomDZuKRL3X15CG0DgN0JuBh4e9Z+/URdk3snWUcMwGhgxAZZkKGRqIzF8LAFYv4ARhVraCIUAQ/zYBN85khDr6DZ4939ByXJzK6W9K+Sznf3u4fSMqSKE0IEIdDp25lbHwYAtiuDIzb0eZC2DMXw0AMxfwCjjEwNhCLQYR70wGeONPQKmt3f/o+7t8zsywTM8oETQgQj0Onb1ZWqasdraqxGg86N1YZqx2uqrlSzUfoKADaTsREb+jwAtiuDMX8A2DYyNRCKQId50AOfOdLQK2j2JDP7dvx/k3RGfN0kubs/fOCtQyo4IURQApy+XT9RV3O1e9C5udrU8sllgmYAsi1jIzb0eQBsV8Zi/gDQFzI1EJIAh3mwBT5zDNvYZje4e8HdHx7/O8vdT+v4PwGzgPU6IQSQvsm9kyqOF7u2FceLmtjDNEEAGdcesSmVJLPoMsURG/o8ALarHfPvRJYGgFHRztSYn5cOH44uyawHMGitlrSwIM3NRZetVtotAranV6YZcipjk8CBXQlxrZrK/oqm9k2dsqZZZT/TBAFkXMZqa9DnAbBdZGmMnhDPA4DdIFMDwDBRCh+jzNw97TYMVblc9qWlpbSbkWns1BCKTH6XEzp7b621VF2pavnksib2TKiyv6LCGD9QAOhHJo8TADKr3Y3LQMwfWwh5/04wEAAwChYWpNnZ7gmKpVKU6UrwHllgZkfdvbzRbWSa4RQZmwQO7Fjm1qpJ8Oy9MFbQzIEZ1jADkF8JjBrS5wHQD7I0RkfmzgMSEnIwEAAQFtaDxSgjaIYNcUKIEGTuAB3q2TsADFuSkxDo8wBAcDJ3HpCQ6HTC1WiYpPh04lOuatVG+nUBAMJDKXyMsrG0GwAAg5K5Bdt7nb0DALavcxKCe/ckBABA7mXuPCAh9aNraja6l9hoNl3Lt6yl1CIAADbWXg+2VJLMokvWg8WoIGgGIFiZO0CHevYOAMPGJAQAQA+ZOw9IyGRrSUXd07WtqHs08QDrtgMAsqVdCn9+Xjp8OLqknDBGBeUZAQQrc2vVtM/e15cTG/WzdwAYNmp9AAB6yNx5gJTIWpyVwvWa0rdV05SaOlNF3aMp1VQ5rSbposG0GwCAHaIUPkaVufvW9wpIuVz2pSVmYQFISftkOTNn70AYWmstVVeqqp+oa3LvpCr7KyqMpfTbSmBQDFtIcE0zAAAGLqnj1sKCWpf/kqrNH9eyJjShZVWKN6lw7XsYkQQAAOiDmR119/KGtxE0AwDsCgECpKy11tL0NdOqHa+pudpUcbyoqX1TWrxicfiBM4I5w8MkBKQsU8H6JHFcB5K3sCDNznZnSJdKUa2qfoJd9DMAAAAS0StoRnlGAMDOceKODKiuVFU7XlNjNRqIaqw2VDteU3WlqpkDQ551Xa1Gv4f2oFijEV2vVpkBnjRqfSBFmQrWd7Rp10E8juvAYPRai7Of41gm604CAACEZSztBgAARlhngMC9O0AADEn9RF3N1e6BqOZqU8snl1NoTI9BMQDB6AzWu7wrWJ+GdhBv9rpZHfrYIc1eN6vpa6bVWmv190Ac14HBaK/F2Wmna3G2J40cPBhdEjADAABIFEEzAMDOESBABkzunVRxvHsgqjhe1MSeHQxE7boxyQ2KtVpRNae5ueiy1efYd9KPA+AhmQrWK8EgHsd1YDAqlShrs1SSzKLLqaloOwAAADKF8owAgJ1rBwg612fY6axZYIcq+yua2jd1Spm0yv4UBqLag2LrS5v1OSiWVIU0Kq0Bg9EO1rfLwkopBuvVO4jXV5lajuvAYFBWEQAAYGQQNAMA7FxCAQJgNwpjBS1esajqSlXLJ5c1sWdiZ2v5JNKYZAbFkloajSXWhieR9aQwMjIVrFeCQTyO68DgsBYnAGDAWq3oXK9ej+ZCMT8D2JnUgmZmdoekuyW1JD3g7mUze5Sk90p6nKQ7JP28u3/TzEzSWyQ9R9I9kn7F3W+JH+eFkg7GD/sH7v6uYb4OYMc4kiEEzJpFRhTGCpo5MNNfRsXAGrP7QbFeFdL6edikHge9tdeTWh9AWbxikcBZoDIVrFeCQTyO6wAAACOJKiNActLONHuGu/9rx/VXSfo7d3+9mb0qvv4/JFUkXRj/m5L0NklTcZDtkKSyJJd01MyOuPs3h/kigL5xJENImDWLgGRlPkNSFdKotDYcnetJSepaTyoTgVwMRJaC9YkG8TiuAwAAjByqjADJGUu7AetcKqmdKfYuST/Xsf3PPfIpSWeb2V5J05JucPdvxIGyGyQ9e9iNBvrWeSRz7z6SAQD612pJCwvS3Fx02Wrt6CGmp6XZWenQoehyenpHD7Vr7QpppZJkFl3upEJaUo+D3nqtJzXSEvhdYXjaQbyDFx/UzIEZshwBAABypFeVEQD9STPTzCVdb2Yu6U/d/SpJ3+3uJyTJ3U+Y2aPj++6T9NWOvz0Wb9tsexczu1LSlZJ0/vnnJ/06gP5RLwsAkpNQ9m6WZuYlVSGNSmvDkdh6UllCVnxusT4fUpeVtG8AAEYIVUaA5KQZNHuau98ZB8ZuMLN/6nFf22Cb99jevSEKyF0lSeVy+ZTbgaHjSAYAyUko2pW1+QxJVUij0trgJbaeVJZkKYqMoWF9PqSOgD0AADvSrjKy/hBKlRGgf6mVZ3T3O+PLr0v6oKSLJH0tLruo+PLr8d2PSTqv48/PlXRnj+1AtlEvCwCSk1AdivZ8hk7MZ8B2tNeTmr9sXoefcVjzl82PfpCB+i651Lk+n8u71ucDhoIy9gAA7Ei7ysj8vHT4cHTJnBNgZ1LJNDOzoqQxd787/v8lkg5LOiLphZJeH19+KP6TI5JebmbXSpqS9K24fOOipP9pZo+M73eJpFcP8aUAO0O9LOAUlIPCjiWUvcvMPOxGez2pmQOBZGGRFT88GSpF12t9vtS+2xl6fzAEWUv7BgBghFBlBEhGWuUZv1vSB82s3Ya/cPe/NbObJb3PzF4i6SuSnh/f/8OSniNpRdI9kl4kSe7+DTObk3RzfL/D7v6N4b0M5FUi5+4cyXpifCRfKAeFXUko2sV8BoRk1xMRiCIPR4Kl6JLoO2VufT5K9eUPAftc4twPAABkibnna4mvcrnsS0tLaTcDI4xz98HjPc6fhdsXNHvdbNcgXWm8pPnL5sPJ2sBgtUdbiHYByU1E4Hc1eAsL0uxsd4CgVIrq6fQxsSqpvlPmJrEk9P5ghHAikDt85AAAIA1mdtTdyxvdltqaZsCoosz+4PEe50+vclDAtrSzdw8ejC4ZZUGOJbYuFb+rwUto7bik+k6ZW5+PtfXyhwVZcodzPwAAkDUEzYA+ce4+eLzH+dMuB9Up1XJQADDCmIgwQtql6DrtoBRdkn2n9vp8By8+qJkDM+mWSU7o/cGIIWCfK5z7AQCArCFoBvSJc/fB4z3On8r+iqb2Tak0XpLJVBovaWrflCr7WTsHAPrFRIQR0l47rlSSzKLLHawdF2zfKaH3B0B2Bbv/AgAAI4s1zYA+UXN98HiP86m11lJ1parlk8ua2DOhyv5KurPbs4YV0gFsU+bWpUJvCawdF3TfibX1gKAFvf8CAACZ1WtNM4JmwA5w7j54vMdAB0YTAPSJiQhDkLHJDPSdAIwq9l8AAGDYCJp1IGgGABg5CwvS7Gy0MnpbqSTNz0drfQA9tIMn9RN1Te6dJHgCJIHJDFti37M53hsAAAAgXb2CZqcNuzEAAKBPvVZIJ2iGHhIt05exrBogVdVqFDBrT2ZoNKLr1Sr7ZVEitBfeG2QBh3QAAIDNETQDkChmzgID0F4hvTPTjBXSsQ3Vlapqx2tqrEbfncZqQ7XjNVVXqpo50MfAPlk1QDcmM/SU2L4nQLw3SBuHdAAAgN7G0m4AgHC0Z87OXjerQx87pNnrZjV9zbRaa620mwaMtkolGs0olSSz6HJqKtoO9FA/UVdztXtgv7na1PLJ5f4eqDOrxr07qwbIo/Zkhk5MZnhQYvueAPHeIG0c0gEAAHojaAYgMZ0zZ13eNXMWwC4UCtH03/l56fDh6JLpwNiGyb2TKo53D+wXx4ua2NPnwH69rtY9DS0ckOYulhYOSK17GlFWTUparWi5v7m56LLF/AwME5MZekps3xMg3pvRE9rxpleiLAAAACjPCCBBvWbOUm4G2KVCISr5Rdkv9KGyv6KpfVOnrJ1T2d/fwH5r4oc0/cKCantaaj5MKt4vTZ0saPFJP6g0QreUlkLq2pMZqtVopHligkWBOiS17wkR781oCfF4Q9VvAACA3giaAUhMe+Zse40GiZmzAJCmwlhBi1csqrpS1fLJZU3smdjRWpPVC6XauVIj/rPGd0TXqxdKaYRxO0tLSd2lpYgrY2iYzLCppPY9IeK9GS0hHm/aibLrA4EkygIAAEQImmGgWmstVVeqqp+oa3LvJCeEgWPmLABkT2GsoJkDM7vK+K1/7TNqFta6tjULa1r++mc1832X9vVYSfQNepWWGtVBTCA0Sex7QsV7MzpCPN6QKAsAANAbQTMMTGutpelrpk8JoCxesUjgLFDMnAWAMCWVSZxU34DSUgCAYQj1eEOiLAAAwObG0m4AwlVdqap2vKbGakMuV2O1odrxmqor1bSbhgFqz5w9ePFBzRyY2XHArLXW0sLtC5q7cU4Lty+otTbiK24DwAhrZxKXxksymUrjpR1lEifVN2iXliqVJLPoktJSAICkcbwBAADIHzLNMDD1E3U1V7trWTRXm1o+uUwpEvREliIAZEtSmcRJ9Q0oLQUAGAaONwAAAPlD0AwDk1QpJ+RPZyaCpK5MBAKuAJCOJNbgSbJvQGkp7BRr7gLoB8cbAACAfCFohoFpl3Jany3Ubykn5A9ZiltrtaIZr/V6tNYCM14BjIIs9g0IoAxBhg5aZLMDAAAAAHohaIaBSaqUE/KHLMXeWi1pelqq1aRmM1qMfGoqKh1D4AxAlmWtb0AAZQgydtAimx0AAAAA0MtY2g1A2NqlnA5efFAzB2YYgMK2tDMRSuMlmUyl8VLqmQhZUq1GY4+NhuQeXdZq0XYAyLos9Q06Aygu7wqgICEZO2j1ymYHAAAAAIBMMwCZk7VMhKyp16PJ+p2azWhxctZaAIDtoxzwEGTsoEU2O5B9lM0FAABAmgiaIV8ytKYGemtnIjBoearJyai6VeOh8T4Vi9IE433JYn8BBC/RAAr7jI1l7KCVxXX1EsN3EAGgbC4AAEB/OA1IHkEz5EfG1tQAdqpSib6667/KlQDG+zKD/QWQC4kFUNhnbC5jB61gs9n5DiIQrDsIYJQxcA1g2DgNGAyCZsiPzjU1pO41NahphxFSKEQHv2o1qm41MUFnPHHsL4BcSCyAwj5jcxk8aAWZzc53EIGgbC6AUcXANYA0cBowGATNMBqSmK6TsTU1gsXUqqEoFKKvLV/dAWF/AeRGIgEU9hm9cdAaPL6DCATrDgIYVQxcA0gDpwGDQdAM2ZfUdJ2MrakRJKZWIRTsLwD0g30G0sZ3EIEIet1BAF1Cm2/LwDWANHAaMBhjaTcA2FLndB337uk6/WivqVEqSWbRJQtBJSupzwpIG/sLAP1gn4G08R1EINplc+cvm9fhZxzW/GXzWrxicfTXHQTQpT3fdnZWOnQoupyejraPqvbAdScGrgEMGqcBg0GmGbIvqek6GVxTIzhMrUIo2F8A6Af7jJ5Cm0meSXwHEZAg1x0E0CXEUobtgev1hXcYuAYwSJwGDAZBM2RfknmmrKkxWOQEIyTsLwD0g33GhqjcPER8BwEAIyLE+bYMXANIC6cByRt6eUYzO8/MPmpmt5nZrWb2ynj7a83suJktx/+e0/E3rzazFTP7gplNd2x/drxtxcxeNezXgm1otaSFBWluLrrcSa49eaa9JfEeJ4XPCgAAdKByMwAAWC/UUobtgeuDB6NLAmYAMJrSyDR7QNJvufstZnaWpKNmdkN825vc/Q2ddzazJ0q6XNL3S3qMpI+Y2YH45rdKepakY5JuNrMj7v75obwKbC2pqcVM19lc1qZv81kBmddaa6m6UlX9RF2TeydV2V9hnZAOvD9AskKcSQ7sVrAlS4N9YQCSRilDAECWDT1o5u4nJJ2I/3+3md0maV+PP7lU0rXufp+kL5vZiqSL4ttW3P1LkmRm18b3JWiWFUkWqSbPdGNZLATOZwVkVmutpel3X6LaVz6h5tq9Ko6drqnzn6rFF1xPYEjx+3PNtGrHa2quNlUcL2pq35QWr1jk/cGWGCveGJWbgW5Zm/OWmGBfGIBBYL4tACDLhl6esZOZPU7SpKRavOnlZvYZM3unmT0y3rZP0lc7/uxYvG2z7Rs9z5VmtmRmS3fddVeCrwA99ZpajGTwHgPoQ/X2BdW+eKMaa/fKJTXW7lXtizeqevtC2k3LhOpKVbXjNTVWG3K5GqsN1Y7XVF2hjhx6a48Vz85Khw5Fl9PT6VZMzgoqNwPdgi1ZGuwLAzAolDIEAGRVakEzMytJuk7Sb7r7tyW9TdL3SJpQlIn2J+27bvDn3mP7qRvdr3L3sruXzznnnF23HdsUapHqLOE9BtCH+k1/qaZ1j+I3raXlm96fUouypX6iruZq90SE5mpTyyeZiIDeGCveXHsm+fy8dPhwdEniCfIs2Dlvwb4wAAAA5E0qQTMze5iigNl73P0DkuTuX3P3lruvSXq7HirBeEzSeR1/fq6kO3tsR1YwtXjweI+B3GittbRw+4LmbpzTwu0Laq31n8IyeUIq3t+9rXi/NHEyoUaOuMm9kyqOd09EKI4XNbGHiQjojbHi3kKdSd5qSQsL0txcdElmIbYj2Dlvwb4wAAAA5M3Q1zQzM5P0Dkm3ufsbO7bvjdc7k6TnSvpc/P8jkv7CzN4o6TGSLpT0aUWZZhea2QWSjku6XNIvDudVYFsoUj14vMdA5iWxzlFSa21VJp+vqQ9eq9qelpoPiwJmUycLqjz3eX2+qjBV9lc0tW/qlPe5sp+JCOiNdbvyh+WbsFPtOW/rvzsjP+ct2BcGAACAvDH3DSsaDu4JzX5M0k2SPitpLd78GkmzikozuqQ7JL20HUQzs9+V9GJJDygq51iNtz9H0pslFSS9091ft9Xzl8tlX1paSvIlAQCwoaQGVRduX9DsdbNqrD40Il8aL2n+snnNHJjpq0Gt6UtUvesTWj77Xk38++mqnPNUFRavZ5Q31lprqbpS1fLJZU3smVBlf6WvwGSyjUkg4oqhIICSPwsL0dp1nYHSUikqPznTx24Z+dTevQc35y3YFwYASAKnNwCyxMyOunt5w9uGHTRLG0EzhKY9wFs/Udfk3sl0B3gBdElqUHXuxjkd+tghecfSnSbT4Wcc1sGLD/bXKAa0RgNRmJHDTytf5uakQ4eiNezazKJ12w72uVsGAAAIHac3ALKmV9Bs6OUZASQnqZJtAAaj1zpH/QTN2mttdWaa7XitrfbiQqRCZFu1Gp1RtiOujUZ0vVrls8soflr5QklOAACQdVnK7OL0BsAoGUu7AcAwhbZge3WlqtrxmhqrDblcjdWGasdrqq5U024aAD00qNppJ4Oq7bW2SuMlmUyl8RJrbYWuV8QVQOrayzeVSlGGWanE8k0YYaGdJGFo+OoA2dXO7JqdjbLjZ2ej62n9TkM+vWFfCISHTDPkRoip4PUTdTVXu3sdzdWmlk8u97fOEYCBqFSki6Za+sRdVd37yLpO/+akLjqnokqlv51OYaygxSsWs7PWFgaPNBbgVBmaLl0oRH1ISnJi5IV4koSh4KsDZFvWMrtCPb1hXwiEiaAZciNrHYYkJFqyDUDyrCW9YFr6Sk1aa0pjRen8KckWJfUfOJs5MENAPC/aaSzrz75IY0FeZXBEgpKcCEK1qtanbla1+ROqa1KTjboqn7pJhVE+ScJQhHh+DYQkqaUCkhLq6Q37QiBMlGdEboSYCk7JNiDbqitVffrOmu5da0hy3bvW0KfvpIQqtqGdxjI/Lx0+HF0yXTEfqO+ysc4RCffuEQkAO9Y6uqzp5nWa1bwO6bWa1bymm9epdcs/pt204IS2ew/x/BoISVJLBSQl1NMb9oVAmMg0Q26EmApOyTYg2yihil0hjSV/MphNlRlZmy4NBKLaukQ1PVENlSRJDZ2lmp6i6gMPF7+sWAKlYUPcvYd4fg2EJIuZXSGe3rAvBMJE0Ay5kcUOQxIo2QZkFyVUAfSF+i6bY0QCGIh6oax14Wg1daaWTysTNJMSi3aFuHsP9fwaCAXrrw4H+0IgTATNkBt0GAAMW7uEau14Tc3VporjRUqoAtgc2VSbY0QCGIjJHx5TseTr4tGmiSdbeo3KkoSiXSHu3jm/BrIvxMyurGFfCISJoBlyhQ4DgGGihCqAvpBNtTlGJICBiOLRti4ebcSj2xKKdoW6e+f8GgDYFwIhMndPuw1DVS6XfWlpKe1mAAAAAN1CXPQGQOa1l+wiHr2BhQVpdrY72lUqSfPzfY2OsnsHAADIFjM76u7lDW8jaAYAAABkBKPXAJAdCUa72L0DAABkB0GzDgTNAAAAAADAthDtAgAACE6voBlrmgEAAAAAAGyExWoAAAByZSztBgAAAAAAAAAAAABpI9MMAAAAAAAgL9olJ+t1aXKSkpMAAAAdCJoBAAAAAADkQaslTU9LtZrUbErFojQ1JS0uEjgDAAAQ5RkBANhQqyUtLEhzc9Flq5V2iwAAAIBdqlajgFmjIblHl7VatB0AAABkmgEAsB4TcAEAABCkej3q4HZqNqXlZWlmJp02AQAAZAiZZgAArMMEXAAAAARpcjKaEdapWJQmJtJpDwAAQMYQNAMAYJ1eE3ABAACAkVWpRCUUSiXJLLqcmoq294t65gAAIECUZwQAYJ32BNxG46FtTMAFAADAyCsUoprj1Wo0I2xiIgqY9VuDnHrmAAAgUGSaAQCwTpITcAEAAIBMKRSi9csOHowudxLkop45AAAIFJlmAACsk9QEXAAAACBIveqZz8yk0yYAAIAEEDQDAGAD7Qm4nPMDAAAA61DPHAAABIryjAAAAAAAANg+6pkDAIBAkWkGAAAAAEhUqxWVOa7Xo4QUyhwDgaGeOQAACBRBMwAA0LfWWkvVlarqJ+qa3Dupyv6KCmMMkoSMAXAA29VqSdPTUq0WLXFULEYJKIuL7DeAoFDPHAAABIigGQAA6EtrraXpa6ZVO15Tc7Wp4nhRU/umtHjFIoGzQDEADqAf1Wq0v2gvddRoRNerVcbWAQAAAGQba5oBALCB1lpLC7cvaO7GOS3cvqDWWivtJmVGdaWq2vGaGqsNuVyN1YZqx2uqrlTTbhoGpHMA3L17ABwA1qvXowB7p2YzquAGAAAAAFk28plmZvZsSW+RVJB0tbu/PuUmBSFrZbcoCTV4WfvMMTpC/H2SSdVb/URdzdXu0dDmalPLJ5c1c4AUghD1GgAnawTYnRD7YJOTUUZqO9NMiq5PTKTXJmwuqe9gUn3CEPuWwG7wm9gc7w2AfrDPwHaNdNDMzAqS3irpWZKOSbrZzI64++fTbdloy9pgMSWhBi9rnzlGR6i/z85MKkldmVQEhaTJvZMqjhcffH8kqThe1MQeRkNDxQA4MBih9sEqlag/sL5/UKmk3TKsl9R3MKk+Yah9S2Cn+E1sjvcGQD/YZ6Afo16e8SJJK+7+JXdflXStpEtTbtPIy1rZLUpCDV7WPnOMjlB/n70yqSBV9lc0tW9KpfGSTKbSeElT+6ZU2c9oaKjaA+ClkmQWXTIADuxeqH2wQiEagJiflw4fji4ZkMimpL6DSfUJQ+1bAjvFb2JzvDcA+sE+A/0Y9aDZPklf7bh+LN7WxcyuNLMlM1u66667hta4UZW1wWLWRBi8rH3mGB2h/j7bmVSdyKR6SGGsoMUrFjV/2bwOP+Ow5i+bH/msCPTGADgwGCH3wQqFqHzrwYPRJfuLbErqO5hUnzDUviWwU/wmNsd7A6Af7DPQj5EuzyjJNtjmp2xwv0rSVZJULpdPuR3dslZ2i5JQg5e1zxyjI9TfZzuTan2pIjKpHlIYK2jmwAzlKnOkPQDOGmZAcuiDIW1JfQeT6hOG2rcEdorfxOZ4bwD0g30G+jHqmWbHJJ3Xcf1cSXem1JZgZK3sFiWhBi9rnzlGR6i/TzKpAADDQB8MaUvqO5hUnzDUviWwU/wmNsd7A6Af7DPQD3Mf3cQrMztN0u2SninpuKT7ocXhAAAJwElEQVSbJf2iu9+62d+Uy2VfWloaUgtHV2utpepKVcsnlzWxZ0KV/ZVUB4tbrajG7PJyNAOgUqHES9Ky9pljdPD7BABg5+iDIW1JfQeT6hPStwS68ZvYHO8NgH6wz0AnMzvq7uUNbxvloJkkmdlzJL1ZUkHSO939db3uT9AMAAAAAAAAAAAgn3oFzUZ9TTO5+4clfTjtdgAAAAAAAAAAAGB0jfqaZgAAAAAAAAAAAMCuETQDAAAAAAAAAABA7hE0AwAAAAAAAAAAQO4RNAMAAAAAAAAAAEDuETQDAAAAAAAAAABA7hE0AwAAAAAAAAAAQO4RNAMAAAAAAAAAAEDuETQDAAAAAAAAAABA7hE0AwAAAAAAAAAAQO6Zu6fdhqEys7sk/Uva7Rgh3yXpX9NuBADgQeyXASBb2C8DQLawXwaAbGG/jCx6rLufs9ENuQuaoT9mtuTu5bTbAQCIsF8GgGxhvwwA2cJ+GQCyhf0yRg3lGQEAAAAAAAAAAJB7BM0AAAAAAAAAAACQewTNsJWr0m4AAKAL+2UAyBb2ywCQLeyXASBb2C9jpLCmGQAAAAAAAAAAAHKPTDMAAAAAAAAAAADkHkEzbMjMnm1mXzCzFTN7VdrtAYC8MbPzzOyjZnabmd1qZq+Mtz/KzG4ws3+OLx+ZdlsBIE/MrGBmdTNbiK9fYGa1eL/8XjMbT7uNAJAnZna2mb3fzP4p7jv/KH1mAEiPmf3XeBzjc2Y2b2an02fGKCFohlOYWUHSWyVVJD1R0qyZPTHdVgFA7jwg6bfc/QmSniLpZfG++FWS/s7dL5T0d/F1AMDwvFLSbR3X/0jSm+L98jclvSSVVgFAfr1F0t+6+/dJepKifTR9ZgBIgZntk/QKSWV3/wFJBUmXiz4zRghBM2zkIkkr7v4ld1+VdK2kS1NuEwDkirufcPdb4v/frejkf5+i/fG74ru9S9LPpdNCAMgfMztX0k9Lujq+bpJ+UtL747uwXwaAITKzh0u6WNI7JMndV93930WfGQDSdJqkM8zsNElnSjoh+swYIQTNsJF9kr7acf1YvA0AkAIze5ykSUk1Sd/t7iekKLAm6dHptQwAcufNkv67pLX4+ndK+nd3fyC+Tr8ZAIbr8ZLukvRncencq82sKPrMAJAKdz8u6Q2SvqIoWPYtSUdFnxkjhKAZNmIbbPOhtwIAIDMrSbpO0m+6+7fTbg8A5JWZzUj6ursf7dy8wV3pNwPA8Jwm6cmS3ubuk5KaohQjAKQmXkPyUkkXSHqMpKKiJYDWo8+MzCJoho0ck3Rex/VzJd2ZUlsAILfM7GGKAmbvcfcPxJu/ZmZ749v3Svp6Wu0DgJx5mqSfNbM7FJUv/0lFmWdnx6VnJPrNADBsxyQdc/dafP39ioJo9JkBIB0/JenL7n6Xu98v6QOSnir6zBghBM2wkZslXWhmF5jZuKLFGo+k3CYAyJV4nZx3SLrN3d/YcdMRSS+M//9CSR8adtsAII/c/dXufq67P05R//jv3f2XJH1U0vPiu7FfBoAhcveTkr5qZt8bb3qmpM+LPjMApOUrkp5iZmfG4xrt/TJ9ZowMcycTEqcys+comjlbkPROd39dyk0CgFwxsx+TdJOkz+qhtXNeo2hds/dJOl9RZ/T57v6NVBoJADllZk+X9NvuPmNmj1eUefYoSXVJV7j7fWm2DwDyxMwmJF0taVzSlyS9SNEkcfrMAJACM/t9Sb8g6QFF/eNfVbSGGX1mjASCZgAAAAAAAAAAAMg9yjMCAAAAAAAAAAAg9wiaAQAAAAAAAAAAIPcImgEAAAAAAAAAACD3CJoBAAAAAAAAAAAg9wiaAQAAAAAAAAAAIPcImgEAAADAAJhZy8yWzexzZvbXZnZ2Btr0mhSf+1fM7P+k9fwAAAAAsBWCZgAAAAAwGP/h7hPu/gOSviHpZWk3SFJqQTMAAAAAyDqCZgAAAAAweJ+UtK99xcx+x8xuNrPPmNnvd2z/XTP7gpl9xMzmzey34+0fM7Ny/P/vMrM74v8XzOyPOx7rpfH2vWb28Y5Mtx83s9dLOiPe9h4zK5rZ35jZP8b3+YX1jY6f903xY91mZj9iZh8ws382sz/ouN9fmdlRM7vVzK7s2P4iM7vdzG6U9LSO7eeY2XVxu282s6cJAAAAAFJ2WtoNAAAAAICQmVlB0jMlvSO+fomkCyVdJMkkHTGziyU1JV0uaVLRudotko5u8fAvkfQtd/8RM/sOSf/PzK6X9J8kLbr76+LnP9PdbzKzl7v7RNyOyyTd6e4/HV9/xCbPseruF5vZKyV9SNIPK8qc+6KZvcnd/03Si939G2Z2hqSbzew6SeOSfj++/7ckfVRSPX7Mt0h6k7v/g5mdL2lR0hO2834CAAAAwKAQNAMAAACAwTjDzJYlPU5R8OuGePsl8b92AKmkKIh2lqQPuvs9kmRmR7bxHJdI+iEze158/RHxY90s6Z1m9jBJf+Xuyxv87WclvcHM/kjSgrvftMlzHOm4/63ufiJu35cknSfp3yS9wsyeG9/vvLgNeyR9zN3viu//XkkH4vv8lKQnmln7OR5uZme5+93beM0AAAAAMBCUZwQAAACAwfiPOKvrsYqyrtprmpmkP4zXO5tw9/3u/o74Nt/ksR7QQ+dvp3dsN0n/peOxLnD3693945IulnRc0rvN7JfXP6C7364oC+yzkv7QzH5vk+e+L75c6/h/+/ppZvZ0RUGwH3X3JykKBrbbuNnrGYvv3273PgJmAAAAANJG0AwAAAAABsjdvyXpFZJ+O878WpT0YjMrSZKZ7TOzR0v6uKTnmtkZZnaWpJ/peJg7FAW4JOl5HdsXJf1G/LgyswPxWmWPlfR1d3+7orKQT47vf3/HfR8j6R53v0bSGzru069HSPqmu99jZt8n6Snx9pqkp5vZd8bP+fyOv7le0svbV8xsYofPDQAAAACJoTwjAAAAAAyYu9fN7B8lXe7u7zazJ0j6ZFyesCHpCne/JS5huCzpXyR1lkt8g6T3mdkLJP19x/arFZV/vMWiB7tL0s9Jerqk3zGz++PHb2eaXSXpM2Z2i6Q/l/THZrYm6X5Jv7HDl/e3kn7dzD4j6QuSPhW/5hNm9lpJn5R0QtEabYX4b14h6a3x35ymKGD46zt8fgAAAABIhLlvVi0DAAAAAJCWOODUcPc3pN0WAAAAAMgDyjMCAAAAAAAAAAAg98g0AwAAAAAAAAAAQO6RaQYAAAAAAAAAAIDcI2gGAAAAAAAAAACA3CNoBgAAAAAAAAAAgNwjaAYAAAAAAAAAAIDcI2gGAAAAAAAAAACA3CNoBgAAAAAAAAAAgNz7/yfnauRJYaAwAAAAAElFTkSuQmCC\n"}]},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597958173577_1190291426","id":"paragraph_1591860123109_1248831991","dateCreated":"2020-08-20 21:16:13.577","status":"READY"},{"text":"%python.ipython\n","user":"anonymous","dateUpdated":"2020-08-20 21:16:13.577","config":{},"settings":{"params":{},"forms":{}},"apps":[],"runtimeInfos":{},"progressUpdateIntervalMs":500,"jobName":"paragraph_1597958173577_538284345","id":"paragraph_1597047441935_258792906","dateCreated":"2020-08-20 21:16:13.577","status":"READY"}],"name":"Log Analysis-Zeppelin","id":"2FJF9WE51","defaultInterpreterGroup":"spark","version":"0.9.0-preview2","noteParams":{},"noteForms":{},"angularObjects":{},"config":{"isZeppelinNotebookCronEnable":false},"info":{}} diff --git a/public/components/notebooks/docs/poc/OpenSearch_Dashboards_Embeddable_Documentation.md b/public/components/notebooks/docs/poc/OpenSearch_Dashboards_Embeddable_Documentation.md deleted file mode 100644 index 2845d22731..0000000000 --- a/public/components/notebooks/docs/poc/OpenSearch_Dashboards_Embeddable_Documentation.md +++ /dev/null @@ -1,55 +0,0 @@ -# OpenSearch Dashboards Embeddable API & Embedding Visualizations - -**NOTE:** The embeddable API and Visualizations have been in high flux for past 6 releases 7.4→7.9 versions in OpenSearch Dashboards - -## **In Version 7.5 and older** - -1. [Elastic blog](https://www.elastic.co/blog/developing-new-kibana-visualizations) on embedding Visualization -2. [Test Plugin](https://github.com/elastic/kibana/tree/7.5/test/plugin_functional/plugins/kbn_tp_visualize_embedding) for OpenSearch Dashboards Visualization embedding - -**Between 7.6 and 7.8 - Embeddable API has changed at a high frequency, better to use it from 7.9** - -## **Embeddable API - Situation post 7.9 update** - -- Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable _containers_. -- Containers are a special type of embeddable that can contain nested embeddables. Embeddables can be dynamically added to embeddable _containers_. _Currently only dashboard uses this interface._ - -![Embeddable API](../dev/images/Embeddable_API.png) - -* [Source](https://github.com/elastic/kibana/issues/19875) -* [Code](https://github.com/elastic/kibana/tree/main/src/plugins/embeddable) -* [README](https://github.com/elastic/kibana/blob/main/src/plugins/embeddable/README.md) - -1. Visualizations, Saved Search and Dashboard embeddable are part of this API now. -2. Embeddable Factory allows to create objects: - 1. with “.create()” menthod → needs input of data/source/query/time range explicitly - 2. with “.createFromSavedObject()” method → either inherits values from containers or takes from explicit input provided -3. Each of the above has a implementation has to inherit an embeddable & Factory API like: - 1. [Viz. Embeddable](https://github.com/elastic/kibana/blob/main/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts) & [Factory](https://github.com/elastic/kibana/blob/main/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx) - 2. [Creating Custom Embeddable Example](https://github.com/elastic/kibana/blob/main/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable.tsx) & [Factory](https://github.com/elastic/kibana/blob/main/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts) by Value - 3. [Creating Custom Embeddable Example](https://github.com/elastic/kibana/blob/main/examples/embeddable_examples/public/todo/todo_ref_embeddable.tsx) & [Factory](https://github.com/elastic/kibana/blob/main/examples/embeddable_examples/public/todo/todo_ref_embeddable_factory.tsx) by reference -4. [Visualizations Embeddable API Code](https://github.com/streamich/kibana/tree/main/src/plugins/visualizations/public/embeddable) -5. [Dashboard Container](https://github.com/elastic/kibana/blob/main/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx) is exposed as an embeddable - to have multiple embeddable in a GRID like structure just like the Dashboard Plugin. - -**Embeddable Examples** - -- Examples folder in OpenSearch Dashboards has all the usage samples for new APIs -- Use to create new embeddable objects -- [Embeddable Examples](https://github.com/elastic/kibana/tree/main/examples/embeddable_examples) shows how to create new embeddable inheriting the API -- [Embeddable Explorer](https://github.com/elastic/kibana/tree/main/examples/embeddable_explorer)shows usage of these embeddable examples in a Panel Container -- [Dashboard Embeddable](https://github.com/elastic/kibana/tree/main/examples/dashboard_embeddable_examples) shows usage of these embeddable examples in a Dashboard Container - -**Embeddable Renderer** - -- The OpenSearch Dashboards react Element/Prop to create new embeddable objects: [Code](https://github.com/elastic/kibana/blob/main/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.tsx) -- Embeddable container use the renderer to create/update each child(an embeddable object) - - [Example Dashboard Container](https://github.com/elastic/kibana/blob/main/src/plugins/dashboard/public/application/embeddable/dashboard_container_by_value_renderer.tsx) - - [Example of Static Embedding](https://github.com/elastic/kibana/blob/main/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx#L59) (without factory) - - [Example of Embedding with factory.create() method](https://github.com/elastic/kibana/blob/main/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx#L73) (with factory) - -## Embedding Visualizations in Notebooks Plugin - -- Notebooks use embeddable API with dashboard containers for embedding visualizations -- Dashboard containers allow loading saved objects by Id -- Notebook paragraphs store the dashboard container object as json string in input cells -- For storing visualizations in Zeppelin input cells, the json string is stored with a prefix “%sh #{JSON_STRING}”. Making the Json object look like a comment so that, it doesn’t interrupt running the whole notebook. diff --git a/public/components/notebooks/docs/poc/Zeppelin_OpenSearch_Storage.md b/public/components/notebooks/docs/poc/Zeppelin_OpenSearch_Storage.md deleted file mode 100644 index 256a9ff487..0000000000 --- a/public/components/notebooks/docs/poc/Zeppelin_OpenSearch_Storage.md +++ /dev/null @@ -1,67 +0,0 @@ -# **Custom OpenSearch Storage in Zeppelin** - -### **Requirement:** - -- Use Zeppelin as a backend service for OpenSearch Dashboards Notebooks and store notebooks as OpenSearch indices -- Use Zeppelin’s storage adaptor interface and implement a new storage adaptor using OpenSearch Client - -### **Design:** - -- [“Transport client API“](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/transport-client.html) is getting deprecated in favor of high level client. -- Finalized, [“High level client API”](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.8/java-rest-high.html) for ease of use and minimal operations needed for Adaptor. -- Notebooks will be indexed as* .notebooks/\_doc/{Unique_id} →* Unique ID is generated by zeppelin - -### **Design Details:** - -1. Implements the interface common for all Zeppelin Storage adaptors -2. Implementation of functions in OpenSearch Zeppelin storage adaptor: - - - Init - Get all config params - - List - List all notebooks - - Get - fetch a notebook - - save - save a notebook - - remove - a note - - close - client connection - - Upgrade client to Https requests - Done using keystore - -### **Usage:** - -1. POC for OpenSearch adapter is stored in branch 'zeppelin-opensearch' of dashboards-notebooks -``` -git checkout zeppelin-opensearch -``` -2. Clone Apache Zeppelin and checkout to 'v0.9.0-preview2' branch in a separate folder -``` -cd /your/folder/ -git clone https://github.com/apache/zeppelin.git -cd zeppelin -git checkout v0.9.0-preview2 -``` -3. Apply patch from dashboards-notebooks -``` -git apply /path/to/zeppelin-patch -``` -4. Once, in this branch copy "opensearch" storage adaptor to your zeppelin files -``` -cp -r /path/to/dashboards-notebooks/zeppelin/zeppelin-plugins/notebookrepo/opensearch path/to/your/zeppelin -``` -4. Add OpenSearch storage property in zeppelin config file "conf/zeppelin-site.xml" and you should comment default git storage -``` - - zeppelin.notebook.storage - org.apache.zeppelin.notebook.repo.OpenSearchNotebookRepo - versioned notebook persistence layer implementation - - - -``` -5. [Build Zeppelin](https://zeppelin.apache.org/docs/0.9.0/setup/basics/how_to_build.html) using Open-JDK 8 -``` - mvn clean package -DskipTests -``` diff --git a/public/components/notebooks/docs/poc/docs/Zeppelin_OpenSearch_Storage.md b/public/components/notebooks/docs/poc/docs/Zeppelin_OpenSearch_Storage.md deleted file mode 100644 index 2449ca6723..0000000000 --- a/public/components/notebooks/docs/poc/docs/Zeppelin_OpenSearch_Storage.md +++ /dev/null @@ -1,73 +0,0 @@ -# **Custom OpenSearch Storage in Zeppelin** - -### **Requirement:** - -- Use Zeppelin as a backend service for OpenSearch Dashboards Notebooks and store notebooks as indices -- Use Zeppelin’s storage adaptor interface and implement a new storage adaptor using Elasticsearch Client - -### **Design:** - -- [“Transport client API“](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/transport-client.html) is getting deprecated in favor of high level client. -- Finalized, [“High level client API”](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.8/java-rest-high.html) for ease of use and minimal operations needed for Adaptor. -- Notebooks will be indexed as* .notebooks/\_doc/{Unique_id} →* Unique ID is generated by zeppelin - -### **Design Details:** - -1. Implements the interface common for all Zeppelin Storage adaptors -2. Implementation of functions in OpenSearch Zeppelin storage adaptor: - - - Init - Get all config params - - List - List all notebooks - - Get - fetch a notebook - - save - save a notebook - - remove - a note - - close - client connection - - Upgrade client to Https requests - Done using keystore - -### **Usage:** - - -1. Clone [dashbaords-notebooks](https://github.com/opensearch-project/dashboards-notebooks/) repository - -2. Clone [Apache Zeppelin](https://github.com/apache/zeppelin) and checkout to 'v0.9.0-preview2' branch in a separate folder - -``` -cd zeppelin -git checkout v0.9.0-preview2 -``` - -3. Apply patch from dashboards-notebooks - -``` -git apply /path/to/dashboards-notebooks/poc/zeppelin-patch -``` - -4. Once, in this branch copy "opensearch" storage adaptor to your zeppelin files - -``` -cp -r /path/to/dashboards-notebooks/poc/zeppelin/zeppelin-plugins/notebookrepo/opensearch path/to/your/zeppelin/zeppelin-plugins/notebookrepo/. -``` - -5. Add OpenSearch storage property in zeppelin config file "conf/zeppelin-site.xml" and you should comment default git storage - -``` - - zeppelin.notebook.storage - org.apache.zeppelin.notebook.repo.OpenSearchNotebookRepo - versioned notebook persistence layer implementation - - - -``` - -6. [Build Zeppelin](https://zeppelin.apache.org/docs/0.9.0/setup/basics/how_to_build.html) using Open-JDK 8 - -``` - mvn clean package -DskipTests -``` diff --git a/public/components/notebooks/docs/poc/zeppelin-patch b/public/components/notebooks/docs/poc/zeppelin-patch deleted file mode 100644 index e6f8611879..0000000000 --- a/public/components/notebooks/docs/poc/zeppelin-patch +++ /dev/null @@ -1,3057 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml -index 13bc6d469..bbdde076a 100644 ---- a/elasticsearch/pom.xml -+++ b/elasticsearch/pom.xml -@@ -23,25 +23,26 @@ - - zeppelin-interpreter-parent - org.apache.zeppelin -- 0.9.0-preview2 -+ 0.9.0-SNAPSHOT - ../zeppelin-interpreter-parent/pom.xml - - - zeppelin-elasticsearch - jar -- 0.9.0-preview2 -+ 0.9.0-SNAPSHOT - Zeppelin: Elasticsearch interpreter - - - elasticsearch -- 2.4.3 -- 4.0.2 -+ 7.8.0 -+ 4.1.4 - 18.0 - 0.1.6 - 1.4.9 - - - -+ - - org.opensearch - elasticsearch -@@ -58,11 +59,11 @@ - commons-lang3 - - -- -- org.apache.httpcomponents -- httpasyncclient -- ${httpasyncclient.version} -- -+ -+ -+ -+ -+ - - - com.google.guava -diff --git a/elasticsearch/src/main/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreter.java b/elasticsearch/src/main/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreter.java -index 45b37c4eb..d7987a011 100644 ---- a/elasticsearch/src/main/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreter.java -+++ b/elasticsearch/src/main/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreter.java -@@ -21,19 +21,6 @@ import com.google.gson.Gson; - import com.google.gson.GsonBuilder; - import com.google.gson.JsonObject; - --import org.apache.commons.lang3.StringUtils; --import org.opensearch.common.xcontent.XContentBuilder; --import org.opensearch.common.xcontent.XContentFactory; --import org.opensearch.common.xcontent.XContentHelper; --import org.opensearch.search.aggregations.Aggregation; --import org.opensearch.search.aggregations.Aggregations; --import org.opensearch.search.aggregations.InternalMultiBucketAggregation; --import org.opensearch.search.aggregations.bucket.InternalSingleBucketAggregation; --import org.opensearch.search.aggregations.bucket.MultiBucketsAggregation; --import org.opensearch.search.aggregations.metrics.InternalMetricsAggregation; --import org.slf4j.Logger; --import org.slf4j.LoggerFactory; -- - import java.io.IOException; - import java.util.ArrayList; - import java.util.Arrays; -@@ -48,7 +35,6 @@ import java.util.Set; - import java.util.TreeSet; - import java.util.regex.Matcher; - import java.util.regex.Pattern; -- - import com.github.wnameless.json.flattener.JsonFlattener; - - import org.apache.zeppelin.completer.CompletionType; -@@ -57,12 +43,25 @@ import org.apache.zeppelin.elasticsearch.action.AggWrapper; - import org.apache.zeppelin.elasticsearch.action.HitWrapper; - import org.apache.zeppelin.elasticsearch.client.ElasticsearchClient; - import org.apache.zeppelin.elasticsearch.client.HttpBasedClient; --import org.apache.zeppelin.elasticsearch.client.TransportBasedClient; - import org.apache.zeppelin.interpreter.Interpreter; - import org.apache.zeppelin.interpreter.InterpreterContext; - import org.apache.zeppelin.interpreter.InterpreterResult; - import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; - -+import org.apache.commons.lang3.StringUtils; -+import org.opensearch.common.Strings; -+import org.opensearch.common.xcontent.ToXContent; -+import org.opensearch.common.xcontent.XContentBuilder; -+import org.opensearch.common.xcontent.XContentFactory; -+import org.opensearch.search.aggregations.Aggregation; -+import org.opensearch.search.aggregations.Aggregations; -+import org.opensearch.search.aggregations.InternalMultiBucketAggregation; -+import org.opensearch.search.aggregations.bucket.InternalSingleBucketAggregation; -+import org.opensearch.search.aggregations.bucket.MultiBucketsAggregation; -+import org.opensearch.search.aggregations.metrics.InternalNumericMetricsAggregation; -+import org.slf4j.Logger; -+import org.slf4j.LoggerFactory; -+ - /** - * Elasticsearch Interpreter for Zeppelin. - */ -@@ -70,25 +69,25 @@ public class ElasticsearchInterpreter extends Interpreter { - private static Logger logger = LoggerFactory.getLogger(ElasticsearchInterpreter.class); - - private static final String HELP = "Elasticsearch interpreter:\n" -- + "General format: ///