[
  {
    "path": ".circleci/config.yml",
    "content": "version: 2.1\n\nparameters:\n  ssh-fingerprint:\n    type: string\n    default: ${GITHUB_SSH_FINGERPRINT}\n\naliases:\n  # Workflow filters\n  - &filter-only-master\n    branches:\n      only: master\n  - &filter-only-release\n    branches:\n      only: /^v[1-9]*[0-9]+\\.[1-9]*[0-9]+\\.x$/\n\nworkflows:\n  plugin_workflow:\n    jobs:\n      - build\n\nexecutors:\n  default_exec: # declares a reusable executor\n    docker:\n      - image: srclosson/grafana-plugin-ci-alpine:latest\n  e2e_exec:\n    docker:\n      - image: srclosson/grafana-plugin-ci-e2e:latest\n\njobs:\n  build:\n    executor: default_exec\n    steps:\n      - checkout\n      - restore_cache:\n          name: restore node_modules\n          keys:\n          - build-cache-{.Environment.CACHE_VERSION }}-{{ checksum \"yarn.lock\" }}\n      - run:\n          name: Install dependencies\n          command: |\n            mkdir ci\n            [ -f ~/project/node_modules/.bin/grafana-toolkit ] || yarn install --frozen-lockfile\n      - save_cache:\n          name: save node_modules\n          paths:\n            - ~/project/node_modules\n          key: build-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum \"yarn.lock\" }}\n      - run:\n          name: Build and test frontend\n          command: ./node_modules/.bin/grafana-toolkit plugin:ci-build\n      - run:\n          name: Move results to ci folder\n          command: ./node_modules/.bin/grafana-toolkit plugin:ci-build --finish\n      - run:\n          name: Package distribution\n          command: |\n            ./node_modules/.bin/grafana-toolkit plugin:ci-package\n      - persist_to_workspace:\n          root: .\n          paths:\n          - ci/jobs/package\n          - ci/packages\n          - ci/dist\n          - ci/grafana-test-env\n      - store_artifacts:\n          path: ci\n"
  },
  {
    "path": ".codeclimate.yml",
    "content": "exclude_patterns:\n- \"dist\"\n- \"**/node_modules/\"\n- \"**/*.d.ts\"\n- \"src/libs/\"\n- \"src/images/\"\n- \"src/img/\"\n- \"src/screenshots/\"\n"
  },
  {
    "path": ".config/.eslintrc",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in\n * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-eslint-config\n */\n{\n  \"extends\": [\"@grafana/eslint-config\"],\n  \"root\": true,\n  \"rules\": {\n    \"react/prop-types\": \"off\"\n  }\n}\n"
  },
  {
    "path": ".config/.prettierrc.js",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in .config/README.md\n */\n\nmodule.exports = {\n  \"endOfLine\": \"auto\",\n  \"printWidth\": 120,\n  \"trailingComma\": \"es5\",\n  \"semi\": true,\n  \"jsxSingleQuote\": false,\n  \"singleQuote\": true,\n  \"useTabs\": false,\n  \"tabWidth\": 2\n};"
  },
  {
    "path": ".config/Dockerfile",
    "content": "ARG grafana_version=latest\nARG grafana_image=grafana-enterprise\n\nFROM grafana/${grafana_image}:${grafana_version}\n\n# Make it as simple as possible to access the grafana instance for development purposes\n# Do NOT enable these settings in a public facing / production grafana instance\nENV GF_AUTH_ANONYMOUS_ORG_ROLE \"Admin\"\nENV GF_AUTH_ANONYMOUS_ENABLED \"true\"\nENV GF_AUTH_BASIC_ENABLED \"false\"\n# Set development mode so plugins can be loaded without the need to sign\nENV GF_DEFAULT_APP_MODE \"development\"\n\n# Inject livereload script into grafana index.html\nUSER root\nRUN sed -i 's/<\\/body><\\/html>/<script src=\\\"http:\\/\\/localhost:35729\\/livereload.js\\\"><\\/script><\\/body><\\/html>/g' /usr/share/grafana/public/views/index.html"
  },
  {
    "path": ".config/README.md",
    "content": "# Default build configuration by Grafana\n\n**This is an auto-generated directory and is not intended to be changed! ⚠️**\n\nThe `.config/` directory holds basic configuration for the different tools\nthat are used to develop, test and build the project. In order to make it updates easier we ask you to\nnot edit files in this folder to extend configuration.\n\n## How to extend the basic configs?\n\nBear in mind that you are doing it at your own risk, and that extending any of the basic configuration can lead\nto issues around working with the project.\n\n### Extending the ESLint config\n\nEdit the `.eslintrc` file in the project root in order to extend the ESLint configuration.\n\n**Example:**\n\n```json\n{\n  \"extends\": \"./.config/.eslintrc\",\n  \"rules\": {\n    \"react/prop-types\": \"off\"\n  }\n}\n```\n\n---\n\n### Extending the Prettier config\n\nEdit the `.prettierrc.js` file in the project root in order to extend the Prettier configuration.\n\n**Example:**\n\n```javascript\nmodule.exports = {\n  // Prettier configuration provided by Grafana scaffolding\n  ...require('./.config/.prettierrc.js'),\n\n  semi: false,\n};\n```\n\n---\n\n### Extending the Jest config\n\nThere are two configuration in the project root that belong to Jest: `jest-setup.js` and `jest.config.js`.\n\n**`jest-setup.js`:** A file that is run before each test file in the suite is executed. We are using it to\nset up the Jest DOM for the testing library and to apply some polyfills. ([link to Jest docs](https://jestjs.io/docs/configuration#setupfilesafterenv-array))\n\n**`jest.config.js`:** The main Jest configuration file that extends the Grafana recommended setup. ([link to Jest docs](https://jestjs.io/docs/configuration))\n\n#### ESM errors with Jest\n\nA common issue found with the current jest config involves importing an npm package which only offers an ESM build. These packages cause jest to error with `SyntaxError: Cannot use import statement outside a module`. To work around this we provide a list of known packages to pass to the `[transformIgnorePatterns](https://jestjs.io/docs/configuration#transformignorepatterns-arraystring)` jest configuration property. If need be this can be extended in the following way:\n\n```javascript\nprocess.env.TZ = 'UTC';\nconst { grafanaESModules, nodeModulesToTransform } = require('./config/jest/utils');\n\nmodule.exports = {\n  // Jest configuration provided by Grafana\n  ...require('./.config/jest.config'),\n  // Inform jest to only transform specific node_module packages.\n  transformIgnorePatterns: [nodeModulesToTransform([...grafanaESModules, 'packageName'])],\n};\n```\n\n---\n\n### Extending the TypeScript config\n\nEdit the `tsconfig.json` file in the project root in order to extend the TypeScript configuration.\n\n**Example:**\n\n```json\n{\n  \"extends\": \"./.config/tsconfig.json\",\n  \"compilerOptions\": {\n    \"preserveConstEnums\": true\n  }\n}\n```\n\n---\n\n### Extending the Webpack config\n\nFollow these steps to extend the basic Webpack configuration that lives under `.config/`:\n\n#### 1. Create a new Webpack configuration file\n\nCreate a new config file that is going to extend the basic one provided by Grafana.\nIt can live in the project root, e.g. `webpack.config.ts`.\n\n#### 2. Merge the basic config provided by Grafana and your custom setup\n\nWe are going to use [`webpack-merge`](https://github.com/survivejs/webpack-merge) for this.\n\n```typescript\n// webpack.config.ts\nimport type { Configuration } from 'webpack';\nimport { merge } from 'webpack-merge';\nimport grafanaConfig from './.config/webpack/webpack.config';\n\nconst config = async (env): Promise<Configuration> => {\n  const baseConfig = await grafanaConfig(env);\n\n  return merge(baseConfig, {\n    // Add custom config here...\n    output: {\n      asyncChunks: true,\n    },\n  });\n};\n\nexport default config;\n```\n\n#### 3. Update the `package.json` to use the new Webpack config\n\nWe need to update the `scripts` in the `package.json` to use the extended Webpack configuration.\n\n**Update for `build`:**\n\n```diff\n-\"build\": \"webpack -c ./.config/webpack/webpack.config.ts --env production\",\n+\"build\": \"webpack -c ./webpack.config.ts --env production\",\n```\n\n**Update for `dev`:**\n\n```diff\n-\"dev\": \"webpack -w -c ./.config/webpack/webpack.config.ts --env development\",\n+\"dev\": \"webpack -w -c ./webpack.config.ts --env development\",\n```\n\n### Configure grafana image to use when running docker\n\nBy default `grafana-enterprise` will be used as the docker image for all docker related commands. If you want to override this behaviour simply alter the `docker-compose.yaml` by adding the following build arg `grafana_image`. \n\n**Example:**\n\n```yaml\nversion: '3.7'\n\nservices:\n  grafana:\n    container_name: 'myorg-basic-app'\n    build:\n      context: ./.config\n      args:\n        grafana_version: ${GRAFANA_VERSION:-9.1.2}\n        grafana_image: ${GRAFANA_IMAGE:-grafana}\n```\n\nIn this example we are assigning the environment variable `GRAFANA_IMAGE` to the build arg `grafana_image` with a default value of `grafana`. This will give you the possibility to set the value while running the docker-compose commands which might be convinent in some scenarios.\n\n---\n"
  },
  {
    "path": ".config/jest/mocks/react-inlinesvg.tsx",
    "content": "// Due to the grafana/ui Icon component making fetch requests to\n// `/public/img/icon/<icon_name>.svg` we need to mock react-inlinesvg to prevent\n// the failed fetch requests from displaying errors in console.\n\nimport React from 'react';\n\ntype Callback = (...args: any[]) => void;\n\nexport interface StorageItem {\n  content: string;\n  queue: Callback[];\n  status: string;\n}\n\nexport const cacheStore: { [key: string]: StorageItem } = Object.create(null);\n\nconst SVG_FILE_NAME_REGEX = /(.+)\\/(.+)\\.svg$/;\n\nconst InlineSVG = ({ src }: { src: string }) => {\n  // testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`)\n  const testId = src.replace(SVG_FILE_NAME_REGEX, '$2');\n  return <svg xmlns=\"http://www.w3.org/2000/svg\" data-testid={testId} viewBox=\"0 0 24 24\" />;\n};\n\nexport default InlineSVG;\n"
  },
  {
    "path": ".config/jest/utils.js",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in .config/README.md\n */\n\n/*\n * This utility function is useful in combination with jest `transformIgnorePatterns` config\n * to transform specific packages (e.g.ES modules) in a projects node_modules folder.\n */\nconst nodeModulesToTransform = (moduleNames) => `node_modules\\/(?!.*(${moduleNames.join('|')})\\/.*)`;\n\n// Array of known nested grafana package dependencies that only bundle an ESM version\nconst grafanaESModules = [\n  '.pnpm', // Support using pnpm symlinked packages\n  '@grafana/schema',\n  'd3',\n  'd3-color',\n  'd3-force',\n  'd3-interpolate',\n  'd3-scale-chromatic',\n  'ol',\n  'react-colorful',\n  'rxjs',\n  'uuid',\n];\n\nmodule.exports = {\n  nodeModulesToTransform,\n  grafanaESModules,\n};\n"
  },
  {
    "path": ".config/jest-setup.js",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in\n * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config\n */\n\nimport '@testing-library/jest-dom';\n\n// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom\nObject.defineProperty(global, 'matchMedia', {\n  writable: true,\n  value: jest.fn().mockImplementation((query) => ({\n    matches: false,\n    media: query,\n    onchange: null,\n    addListener: jest.fn(), // deprecated\n    removeListener: jest.fn(), // deprecated\n    addEventListener: jest.fn(),\n    removeEventListener: jest.fn(),\n    dispatchEvent: jest.fn(),\n  })),\n});\n\nHTMLCanvasElement.prototype.getContext = () => {};\n"
  },
  {
    "path": ".config/jest.config.js",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in\n * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config\n */\n\nconst path = require('path');\nconst { grafanaESModules, nodeModulesToTransform } = require('./jest/utils');\n\nmodule.exports = {\n  moduleNameMapper: {\n    '\\\\.(css|scss|sass)$': 'identity-obj-proxy',\n    'react-inlinesvg': path.resolve(__dirname, 'jest', 'mocks', 'react-inlinesvg.tsx'),\n  },\n  modulePaths: ['<rootDir>/src'],\n  setupFilesAfterEnv: ['<rootDir>/jest-setup.js'],\n  testEnvironment: 'jest-environment-jsdom',\n  testMatch: [\n    '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',\n    '<rootDir>/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}',\n    '<rootDir>/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}',\n  ],\n  transform: {\n    '^.+\\\\.(t|j)sx?$': [\n      '@swc/jest',\n      {\n        sourceMaps: 'inline',\n        jsc: {\n          parser: {\n            syntax: 'typescript',\n            tsx: true,\n            decorators: false,\n            dynamicImport: true,\n          },\n        },\n      },\n    ],\n  },\n  // Jest will throw `Cannot use import statement outside module` if it tries to load an\n  // ES module without it being transformed first. ./config/README.md#esm-errors-with-jest\n  transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)],\n};\n"
  },
  {
    "path": ".config/tsconfig.json",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in\n * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-typescript-config\n */\n {\n  \"compilerOptions\": {\n    \"alwaysStrict\": true,\n    \"declaration\": false,\n    \"rootDir\": \"../src\",\n    \"baseUrl\": \"../src\",\n    \"typeRoots\": [\"../node_modules/@types\"],\n    \"resolveJsonModule\": true\n  },\n  \"ts-node\": {\n    \"compilerOptions\": {\n      \"module\": \"commonjs\",\n      \"target\": \"es5\",\n      \"esModuleInterop\": true\n    },\n    \"transpileOnly\": true\n  },\n  \"include\": [\"../src\", \"./types\"],\n  \"extends\": \"@grafana/tsconfig\"\n}\n"
  },
  {
    "path": ".config/types/custom.d.ts",
    "content": "// Image declarations\ndeclare module '*.gif' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.jpg' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.jpeg' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.png' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.webp' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.svg' {\n  const content: string;\n  export default content;\n}\n\n// Font declarations\ndeclare module '*.woff';\ndeclare module '*.woff2';\ndeclare module '*.eot';\ndeclare module '*.ttf';\ndeclare module '*.otf';\n"
  },
  {
    "path": ".config/webpack/constants.ts",
    "content": "export const SOURCE_DIR = 'src';\nexport const DIST_DIR = 'dist';\n"
  },
  {
    "path": ".config/webpack/utils.ts",
    "content": "import fs from 'fs';\nimport process from 'process';\nimport os from 'os';\nimport path from 'path';\nimport util from 'util';\nimport { glob } from 'glob';\nimport { SOURCE_DIR } from './constants';\n\n\nexport function isWSL() {\n  if (process.platform !== 'linux') {\n    return false;\n  }\n\n  if (os.release().toLowerCase().includes('microsoft')) {\n    return true;\n  }\n\n  try {\n    return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft');\n  } catch {\n    return false;\n  }\n} \n\nexport function getPackageJson() {\n  return require(path.resolve(process.cwd(), 'package.json'));\n}\n\nexport function getPluginJson() {\n  return require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`));\n}\n\nexport function hasReadme() {\n  return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md'));\n}\n\n// Support bundling nested plugins by finding all plugin.json files in src directory\n// then checking for a sibling module.[jt]sx? file.\nexport async function getEntries(): Promise<Record<string, string>> {\n  const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true });\n\n  const plugins = await Promise.all(pluginsJson.map((pluginJson) => {\n      const folder = path.dirname(pluginJson);\n      return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true });\n    })\n  );\n\n  return plugins.reduce((result, modules) => {\n    return modules.reduce((result, module) => {\n      const pluginPath = path.dirname(module);\n      const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\\/?/i, '');\n      const entryName = pluginName === '' ? 'module' : `${pluginName}/module`;\n\n      result[entryName] = module;\n      return result;\n    }, result);\n  }, {});\n}\n"
  },
  {
    "path": ".config/webpack/webpack.config.ts",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in\n * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-webpack-config\n */\n\nimport CopyWebpackPlugin from 'copy-webpack-plugin';\nimport ESLintPlugin from 'eslint-webpack-plugin';\nimport ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';\nimport LiveReloadPlugin from 'webpack-livereload-plugin';\nimport path from 'path';\nimport ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';\nimport { Configuration } from 'webpack';\n\nimport { getPackageJson, getPluginJson, hasReadme, getEntries, isWSL } from './utils';\nimport { SOURCE_DIR, DIST_DIR } from './constants';\n\nconst pluginJson = getPluginJson();\n\nconst config = async (env): Promise<Configuration> => {\n  const baseConfig: Configuration = {\n    cache: {\n      type: 'filesystem',\n      buildDependencies: {\n        config: [__filename],\n      },\n    },\n\n    context: path.join(process.cwd(), SOURCE_DIR),\n\n    devtool: env.production ? 'source-map' : 'eval-source-map',\n\n    entry: await getEntries(),\n\n    externals: [\n      'lodash',\n      'jquery',\n      'moment',\n      'slate',\n      'emotion',\n      '@emotion/react',\n      '@emotion/css',\n      'prismjs',\n      'slate-plain-serializer',\n      '@grafana/slate-react',\n      'react',\n      'react-dom',\n      'react-redux',\n      'redux',\n      'rxjs',\n      'react-router',\n      'react-router-dom',\n      'd3',\n      'angular',\n      '@grafana/ui',\n      '@grafana/runtime',\n      '@grafana/data',\n\n      // Mark legacy SDK imports as external if their name starts with the \"grafana/\" prefix\n      ({ request }, callback) => {\n        const prefix = 'grafana/';\n        const hasPrefix = (request) => request.indexOf(prefix) === 0;\n        const stripPrefix = (request) => request.substr(prefix.length);\n\n        if (hasPrefix(request)) {\n          return callback(undefined, stripPrefix(request));\n        }\n\n        callback();\n      },\n    ],\n\n    mode: env.production ? 'production' : 'development',\n\n    module: {\n      rules: [\n        {\n          exclude: /(node_modules)/,\n          test: /\\.[tj]sx?$/,\n          use: {\n            loader: 'swc-loader',\n            options: {\n              jsc: {\n                baseUrl: './src',\n                target: 'es2015',\n                loose: false,\n                parser: {\n                  syntax: 'typescript',\n                  tsx: true,\n                  decorators: false,\n                  dynamicImport: true,\n                },\n              },\n            },\n          },\n        },\n        {\n          test: /\\.css$/,\n          use: [\"style-loader\", \"css-loader\"]\n        },\n        {\n          test: /\\.s[ac]ss$/,\n          use: ['style-loader', 'css-loader', 'sass-loader'],\n        },\n        {\n          test: /\\.(png|jpe?g|gif|svg)$/,\n          type: 'asset/resource',\n          generator: {\n            // Keep publicPath relative for host.com/grafana/ deployments\n            publicPath: `public/plugins/${pluginJson.id}/img/`,\n            outputPath: 'img/',\n            filename: Boolean(env.production) ? '[hash][ext]' : '[name][ext]',\n          },\n        },\n        {\n          test: /\\.(woff|woff2|eot|ttf|otf)(\\?v=\\d+\\.\\d+\\.\\d+)?$/,\n          type: 'asset/resource',\n          generator: {\n            // Keep publicPath relative for host.com/grafana/ deployments\n            publicPath: `public/plugins/${pluginJson.id}/fonts/`,\n            outputPath: 'fonts/',\n            filename: Boolean(env.production) ? '[hash][ext]' : '[name][ext]',\n          },\n        },\n      ],\n    },\n\n    output: {\n      clean: {\n        keep: new RegExp(`(.*?_(amd64|arm(64)?)(.exe)?|go_plugin_build_manifest)`),\n      },\n      filename: '[name].js',\n      library: {\n        type: 'amd',\n      },\n      path: path.resolve(process.cwd(), DIST_DIR),\n      publicPath: '/',\n    },\n\n    plugins: [\n      new CopyWebpackPlugin({\n        patterns: [\n          // If src/README.md exists use it; otherwise the root README\n          // To `compiler.options.output`\n          { from: hasReadme() ? 'README.md' : '../README.md', to: '.', force: true },\n          { from: 'plugin.json', to: '.' },\n          { from: '../LICENSE', to: '.' },\n          { from: '../CHANGELOG.md', to: '.', force: true },\n          { from: '**/*.json', to: '.' }, // TODO<Add an error for checking the basic structure of the repo>\n          { from: '**/*.svg', to: '.', noErrorOnMissing: true }, // Optional\n          { from: '**/*.png', to: '.', noErrorOnMissing: true }, // Optional\n          { from: '**/*.html', to: '.', noErrorOnMissing: true }, // Optional\n          { from: 'img/**/*', to: '.', noErrorOnMissing: true }, // Optional\n          { from: 'libs/**/*', to: '.', noErrorOnMissing: true }, // Optional\n          { from: 'static/**/*', to: '.', noErrorOnMissing: true }, // Optional\n        ],\n      }),\n      // Replace certain template-variables in the README and plugin.json\n      new ReplaceInFileWebpackPlugin([\n        {\n          dir: DIST_DIR,\n          files: ['plugin.json', 'README.md'],\n          rules: [\n            {\n              search: /\\%VERSION\\%/g,\n              replace: getPackageJson().version,\n            },\n            {\n              search: /\\%TODAY\\%/g,\n              replace: new Date().toISOString().substring(0, 10),\n            },\n            {\n              search: /\\%PLUGIN_ID\\%/g,\n              replace: pluginJson.id,\n            },\n          ],\n        },\n      ]),\n      new ForkTsCheckerWebpackPlugin({\n        async: Boolean(env.development),\n        issue: {\n          include: [{ file: '**/*.{ts,tsx}' }],\n        },\n        typescript: { configFile: path.join(process.cwd(), 'tsconfig.json') },\n      }),\n      new ESLintPlugin({\n        extensions: ['.ts', '.tsx'],\n        lintDirtyModulesOnly: Boolean(env.development), // don't lint on start, only lint changed files\n      }),\n      ...(env.development ? [new LiveReloadPlugin()] : []),\n    ],\n\n    resolve: {\n      extensions: ['.js', '.jsx', '.ts', '.tsx'],\n      // handle resolving \"rootDir\" paths\n      modules: [path.resolve(process.cwd(), 'src'), 'node_modules'],\n      unsafeCache: true,\n    },\n  }\n\n  if(isWSL()) {\n    baseConfig.watchOptions = {\n      poll: 3000,\n      ignored: /node_modules/,\n    }}\n\n\n  return baseConfig;\n\n};\n\nexport default config;\n"
  },
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nmax_line_length = 120\n\n[*.{js,ts,tsx,scss}]\nquote_type = single\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"extends\": \"./.config/.eslintrc\"\n}"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[BUG] - *ENTER TITLE*\"\nlabels: bug\nassignees: ''\n\n---\n\nPlease provide as much and as detailed information as possible as it will make it easier for us to reproduce and fix the bug!\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nA clear and concise description of how to reproduce the bug.\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Your Setup:**\n- OS Grafana is running on:\n- OS & Browser from which Grafana is accessed:\n- Plugin-Version:\n- Grafana-Version:\n- Datasource & Version:\n- Other: \n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[Feature] - *ENTER TITLE*\"\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.md",
    "content": "---\nname: Question\nabout: Ask a question about the project.\ntitle: \"[Question] - *ENTER TITLE*\"\nlabels: question\nassignees: ''\n\n---\n\n**Write your question here**\n\n**Screenshots**\n\n**Where and how could we improve the readme?**\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build plugin\n\non:\n  push:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    container:\n      image: node:20.19\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: Build plugin\n        run: |\n          yarn install && yarn build\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\nnpm-debug.log\n*.log\n.vscode\ndist\ncoverage"
  },
  {
    "path": ".nvmrc",
    "content": "16"
  },
  {
    "path": ".prettierrc.js",
    "content": "module.exports = {\n  // Prettier configuration provided by Grafana scaffolding\n  ...require(\"./.config/.prettierrc.js\")\n};"
  },
  {
    "path": ".release-it.json",
    "content": "{\n    \"github\": {\n      \"release\": false\n    },\n    \"npm\": {\n      \"publish\": false\n    },\n    \"hooks\": {\n      \"after:bump\":\n        \"rm -r dist; yarn build; npx @grafana/toolkit plugin:sign; cp -r dist novatec-sdg-panel; mkdir -p releases; zip -r releases/novatec-sdg-panel.zip novatec-sdg-panel; rm -r novatec-sdg-panel\"\n    }\n  }"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to this project will be documented in this file.\n\n## v4.2.0\n\nUpdate dependencies\n\n## v4.1.2\n\nAdd layering mechanism\nAdd collision between edge-labels\nUpdate dependencies\n\n## v4.0.2\n\nBug fix in icon path\n\n\n## v4.0.1\n\nSumTimings now working as expected\nResponse Time Health now displayed in node statistics\nExternal Icons now use the correct path\n\n\n## v4.0.0\n\nPorted project to react.\naggregationType is not needed as template variable anymore.\nUnit type of data now can be chosen.\nTables of Incoming/Outgoing Statistics are now sortable.\nSettings needed for the dummy data to be displayed is now filled in automatically when dummy data is activated.\nService Icons can now be customized for both Internal and External Services.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n## Releasing\n\nTo create a new release of the plugin, follow these steps.\n\nYou may also read the [official documentation](https://grafana.com/developers/plugin-tools/publish-a-plugin/publish-a-plugin).\n\n### 1. Push a new tag\n\nPush the current state of the plugin to the remote GitHub repository.\n\n  `git tag <version>`\n\n  `git tag push origin -tag <version>`\n\n### 2. Create a release\n\nCreate a new release from the pushed tag manually in GitHub. \nAdd appropriate release notes.\n\n### 3. Add the signed plugin to the release\n\nTo sign a plugin, you will need a **plugin signing token**, \nwhich has been created in our Grafana Cloud account.\n\nSet the environment variable `GRAFANA_ACCESS_POLICY_TOKEN` via\n\n`export GRAFANA_ACCESS_POLICY_TOKEN=<token>` \n\nor in Windows \n\n`set GRAFANA_ACCESS_POLICY_TOKEN=<token>`\n\nThen run the release script to create a signed plugin:\n\n`./scripts/create-signed-plugin.sh`\n\nAdd the created zip file in the `/release` directory to the GitHub release.\n\n### 4. Submit the plugin in Grafana\n\nIf you want to publish the release in the Grafana marketplace, you will have to submit the\nrelease in our Grafana Cloud account.\n\nYou will need to provide:\n\n- URL of the plugin (link to the zip file of the release)\n- SHA1 of the plugin (SHA1 hash of the zip file)\n- Source code URL (URL of the repository for the release tag)\n- Test description (use the `docker-compose.yml` to run the plugin with dummy data)\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   Copyright 2025 Novatec Consulting GmbH\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "## Novatec Service Dependency Graph Panel\n\n![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgrafana.com%2Fapi%2Fplugins%2Fnovatec-sdg-panel&query=%24.downloads&color=orange&label=downloads)\n[![License](https://img.shields.io/github/license/NovatecConsulting/novatec-service-dependency-graph-panel)](LICENSE)\n\n\n![SDG_PRESENTATION](https://user-images.githubusercontent.com/53812669/173822816-da6791ec-c785-435b-a235-21ead3ebd4e1.gif)\n\n\n**Version 4.2.0 is only compatible with Grafana from version 10.4.0!**\n\n**Version 4.0.0 is only compatible with Grafana from version 7.1.0!**\n\n\n\nThe Service Dependency Graph Panel by [Novatec](https://www.novatec-gmbh.de/en/) provides you with many features such as monitoring \nyour latencies, errors and requests of your desired service. This interactive panel for [Grafana](https://grafana.com/) will help you\nvisualize the processes of your application much better. \n\n\n### Updating the Service Dependency Graph Panel\nThe file structure for the icon mapping has changed for version 4.0.0. **Icons are now located in the path 'plugins/novatec-sdg-panel/assets/icons/'.** This also applies to custom icons!\n\n___\n\n## Configuration of the Data Source\n\n### Using Static Dummy Data\n\nIf you want to get a first impression of this panel without having your own data source yet, the panels provides you some dummy data to play around with.\n\nThe dummy data is basically a snapshot of multiple query results in the table format. You'll find its source [here](https://github.com/NovatecConsulting/novatec-service-dependency-graph-panel/blob/master/src/dummy_data_frame.ts), in the panel's GitHub repository.\n\nDepending on the query result, the data provides the following tags:\n* **service**: The service (application) the data is related to.\n* **namespace**: The namespace of a service. Every literal divided by \".\" corresponds to one level of a namespace. For instance **demo.infrastructure**.\n* **protocol**: The communication type (e.g. HTTP, JMS, ...).\n* **origin_service**: In case of an incoming communication, this is the origin service.\n* **target_service**: In case of an outgoing communication, this is the target service.\n* **origin_external**: The origin of an incoming communication, which cannot be correlated to a known service (e.g. HTTP request of a third party application).\n* **target_external**: The target of an outgoing communication, which cannot be correlated to a known service (e.g. third party HTTP endpoint).\n\nDepending on the query result, the data provides the following fields:\n* **in_timesum**: The total sum of all incoming request response times. (Prometheus style)\n* **in_count**: The total amount of incoming requests.\n* **error_in**: The amount of incoming requests which produced an error.\n* **out_timesum**: The total sum of all outgoing request response times. (Prometheus style)\n* **out_count**: The total amount of outgoing requests.\n* **error_out**: The amount of outgoing requests which produced an error.\n* **threshold**: The critical threshold in milliseconds for the response times of incoming requests.\n\n\nIn order to use this data you simply have to activate the Dummy Data Switch you can find in the General Settings. All necessary options will be applied.\nAfter activating the Dummy Data your Data Mapping should look like this:\n\n| key | value |\n| --- | --- |\n| Response Time | in_timesum |\n| Request Rate  | in_count |\n| Error Rate    | error_in |\n| Response Time (Outgoing) | out_timesum |\n| Request Rate (Outgoing) | out_count |\n| Error Rate (Outgoing) | error_out |\n| Response Time Baseline (Upper) | threshold |\n\n\n_Note that you may have to refresh the dashboard or reload the page in order for it to work._\n\n##### Live example dummy data\n\nDownloading and launching the [inspectIT Ocelot demo #1](https://inspectit.github.io/inspectit-ocelot/docs/getting-started/docker-examples) will provide you with live dummy data rather than static one. \nJust open the docker images' Grafana and choose the dashboard `Service Graph` to see the fully functional Service Dependency Graph.\n___\n\n### Use your own Data Source\n\nIf you now want to use your own data source you have make sure, that the data received is in the `TABLE` format and is structured as follows:\n\n* The table requires at least one column which specifies the connection's source or target. The settings `Source Component Column` and `Target Component Column` need to be set to the exact namings of the respective fields.\n \n* The data can contain multiple value columns. These columns have to be mapped on specific attributes using the panel's `Data Mappings` options. \n**Example**: Assuming the data table contains a column named `req_rate` which values represents a request rate for the related connection in the current time window. In order to correctly visualize these values as a request rate, the `Request Rate Column` option has to be set to `req_rate` - the column's name.\n\n#### Examples\n\n##### Example 1\n\nIf the previously described requirements are respected, a minimal table can be as follows:\n\n| app | target_app | req_rate |\n| --- | --- | --- | \n| service a | service b | 50 | \n| service a | service c | 75 |\n| service c | service d | 25 |\n\nAssuming the panel's settings are specified as seen in the screenshot, the panel will visualize the data as following:\n\n![Visualization of the minimal data table.](https://raw.githubusercontent.com/NovatecConsulting/novatec-service-dependency-graph-panel/master/src/img/data-example-1.png)\n\n> Note: It is important to know that connections can only be generated if at least one request-rate column (incoming or outgoing) is defined.\n\n##### Example 2\n\nIn this example, we extend the data table of example 1 by another column, representing the total sum of all request response times of a specific connection (e.g. sum of all HTTP request response times).\n\n| app | target_app | req_rate | resp_time |\n| --- | --- | --- | --- | \n| service a | service b | 50 | 4000 |\n| service a | service c | 75 | 13650 |\n| service c | service d | 25 | 750 |\n\nNow, the panel's `Data Mappings` option `Response Time Column` is set to `resp_time`. This specifies that the value in the `resp_time` column should be handled as the response time for a connection. By default, the values in this column will be handled as a sum of all response times - kind of a Prometheus style metric. This behavior can be changed by using the `Handle Timings as Sums` option. This table will result in the following visualization.\n\n![Visualization of a data table including request rate and response times.](https://raw.githubusercontent.com/NovatecConsulting/novatec-service-dependency-graph-panel/master/src/img/data-example-2.png)\n\n___\n\n## Service Icons\n\nThe service dependency graph plugin allows you to display your own symbols in the drawn nodes.\nFor this purpose the option 'Service Icon Mapping' can be used.\nHere you can specify an assignment of icons to certain name patterns.\nAll nodes that match the specified pattern (regular expression) will get the icon.\n\n![Custom service icons in the graph.](https://raw.githubusercontent.com/NovatecConsulting/novatec-service-dependency-graph-panel/master/src/img/service-icons.png)\n\n##### Example\n\nA sample assignment is included by default: `Pattern: java // Icon: java`.\nThis means that all nodes which have `java` in their name get the `java` icon.\n\n#### Custom Service Icons\n\nYou can add custom icons, by putting them into the plugin's `/assets/icons/` directory.\nThe file type **has to be `PNG`** and the icon itself and **has to be square**.\nIn order to be able to use the icon, its name (without its ending) has to be put into the array contained in the `icon_index.json` file located in the `/assets/icons/` directory.\n\n##### Example\n\nIf the `icon_index.json` has the following content:\n\n```\n[\"java\", \"star_trek\"]\n```\n\nit is assumed that the files `java.png` and `star_trek.png` is existing in the `/assets/icons/` directory.\n___\n\n### Tracing Drilldown\n\nThe service dependency graph plugin allows you to specify a backend URL for each drawn node.\nFor this purpose the option 'Tracing Drilldown' can be used.\nHere you can specify a backend URL. An open and closed curly bracket `{}` is the placeholder for the selected node.\nEach node will get an arrow icon in the details view. This icon is a link to your backend, specified in the options.\nThe curly brackets `{}` will be replaced with the selected node.\n\n#### Example\n\n`http://{}/my/awesome/path` will end up to `http://customers-service/my/awesome/path` when you select the `customers-service`.\n\n___\n\n### Layering\n\nFrom version 4.1.0, the Service Dependency Graph Panel supports layering service nodes by their respective namespace. \n\n####  Setup\nTo use this feature, add a tag containing the namespace of your service to your data. Then set the corresponding option `Namespace Column` in the panel's options to the name of this tag. If you have more than one namespace layer you want to be represented by the panel, you can separate multiple namespaces within your namespace tag by a certain character. This character must be set as the `Namespace Delimiter` in the panel's options. The default delimiter is `.`. Hence, if the content of a namespace column would be `my.awesome.namespace`, the graph would be built with `my` as layer 0, `awesome` as layer 1, and `namespace` as layer 2. Your respective service would then be on layer 3. \n\n#### Usage\nYou can control the layer of your panel by using the (+) and (-) buttons on the panel's top-right. (+) increases the layer currently displayed, (-) decreases the layer. \n___\n\n## Create a Release\n\nTo create a release bundle, ensure `release-it` is installed:\n```\nnpm install --global release-it\n```\nTo build a release bundle:\n```\nrelease-it [--no-git.requireCleanWorkingDir]\n```\n\n### Found a bug? Have a question? Wanting to contribute?\n\nFeel free to open up an issue. We will take care of you and provide as much help as needed. Any suggestions/contributions are being very much appreciated.\n"
  },
  {
    "path": "appveyor.yml",
    "content": "# Test against the latest version of this Node.js version\nenvironment:\n  nodejs_version: \"12\"\n\n# Local NPM Modules\ncache:\n  - node_modules\n\n# Install scripts. (runs after repo cloning)\ninstall:\n  # Get the latest stable version of Node.js or io.js\n  - ps: Install-Product node $env:nodejs_version\n  # install modules\n  - npm install -g yarn --quiet\n  - yarn install --pure-lockfile\n\n# Post-install test scripts.\ntest_script:\n  # Output useful info for debugging.\n  - node --version\n  - npm --version\n\n# Run the build\nbuild_script:\n  - yarn dev   # This will also run prettier!\n  - yarn build # make sure both scripts work\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "services:\n  grafana:\n    container_name: 'novatec-sdg-panel'\n    platform: \"linux/amd64\"\n    build:\n      context: ./.config\n      args:\n        grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise}\n        grafana_version: ${GRAFANA_VERSION:-11.6.0}\n    environment:\n      - GF_PATHS_PROVISIONING=/usr/share/grafana/custom/\n    ports:\n      - 3000:3000/tcp\n    volumes:\n      - ./dist:/var/lib/grafana/plugins/novatec-sdg-panel\n      - ./provisioning:/usr/share/grafana/custom/\n      - ./provisioning/home/home.json:/usr/share/grafana/public/dashboards/home.json\n"
  },
  {
    "path": "docs/README.md",
    "content": "TODO: add example docs structure\n"
  },
  {
    "path": "jest-setup.js",
    "content": "// Jest setup provided by Grafana scaffolding\nimport './.config/jest-setup';\n"
  },
  {
    "path": "jest.config.js",
    "content": "// force timezone to UTC to allow tests to work regardless of local timezone\n// generally used by snapshots, but can affect specific tests\nprocess.env.TZ = 'UTC';\n\nmodule.exports = {\n  // Jest configuration provided by Grafana scaffolding\n  ...require('./.config/jest.config'),\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"novatec-service-dependency-graph-panel\",\n  \"version\": \"4.2.0\",\n  \"description\": \"Service Dependency Graph panel for Grafana\",\n  \"main\": \"src/module.js\",\n  \"scripts\": {\n    \"build\": \"webpack -c ./.config/webpack/webpack.config.ts --env production\",\n    \"dev\": \"webpack -w -c ./.config/webpack/webpack.config.ts --env development\",\n    \"e2e\": \"yarn exec cypress install && yarn exec grafana-e2e run\",\n    \"e2e:update\": \"yarn exec cypress install && yarn exec grafana-e2e run --update-screenshots\",\n    \"lint\": \"eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .\",\n    \"lint:fix\": \"yarn run lint --fix\",\n    \"server\": \"docker-compose up --build\",\n    \"sign\": \"npx --yes @grafana/sign-plugin@latest\",\n    \"test\": \"jest --watch --onlyChanged\",\n    \"test:ci\": \"jest --passWithNoTests --maxWorkers 4\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"keywords\": [\n    \"grafana\",\n    \"plugin\",\n    \"service-dependency-graph\",\n    \"topology\"\n  ],\n  \"repository\": \"github:grafana/NovatecConsulting/novatec-service-dependency-graph-panel\",\n  \"author\": \"Novatec\",\n  \"license\": \"Apache-2.0\",\n  \"bugs\": {\n    \"url\": \"https://github.com/NovatecConsulting/novatec-service-dependency-graph-panel/issues\",\n    \"email\": \"plugins@grafana.com\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.26.10\",\n    \"@babel/preset-env\": \"^7.23.9\",\n    \"@grafana/e2e\": \"^10.1.8\",\n    \"@grafana/e2e-selectors\": \"^10.4.0\",\n    \"@grafana/eslint-config\": \"^6.0.0\",\n    \"@grafana/tsconfig\": \"^1.2.0-rc1\",\n    \"@swc/core\": \"1.3.75\",\n    \"@swc/helpers\": \"^0.5.15\",\n    \"@swc/jest\": \"^0.2.37\",\n    \"@testing-library/jest-dom\": \"^5.16.5\",\n    \"@testing-library/react\": \"^16.2.0\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/lodash\": \"^4.17.16\",\n    \"@types/node\": \"^20.11.17\",\n    \"@types/react-cytoscapejs\": \"^1.2.5\",\n    \"copy-webpack-plugin\": \"^11.0.0\",\n    \"css-loader\": \"^6.7.3\",\n    \"emotion\": \"10.0.27\",\n    \"eslint-webpack-plugin\": \"^4.2.0\",\n    \"fork-ts-checker-webpack-plugin\": \"^8.0.0\",\n    \"glob\": \"^10.4.5\",\n    \"identity-obj-proxy\": \"3.0.0\",\n    \"jest\": \"^29.7.0\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"prettier\": \"^3.5.3\",\n    \"replace-in-file-webpack-plugin\": \"^1.0.6\",\n    \"sass\": \"1.63.2\",\n    \"sass-loader\": \"13.3.1\",\n    \"style-loader\": \"3.3.3\",\n    \"swc-loader\": \"^0.2.6\",\n    \"ts-jest\": \"^28.0.4\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsconfig-paths\": \"^4.2.0\",\n    \"typescript\": \"^5.3.3\",\n    \"webpack\": \"^5.94.0\",\n    \"webpack-cli\": \"^6.0.1\",\n    \"webpack-livereload-plugin\": \"^3.0.2\"\n  },\n  \"engines\": {\n    \"node\": \">=20\"\n  },\n  \"dependencies\": {\n    \"@emotion/css\": \"^11.13.5\",\n    \"@grafana/data\": \"^10.4.0\",\n    \"@grafana/runtime\": \"^10.4.0\",\n    \"@grafana/ui\": \"^10.4.0\",\n    \"@types/react-autosuggest\": \"^10.1.11\",\n    \"@types/react-bootstrap-table-next\": \"^4.0.26\",\n    \"babel-preset-react\": \"^6.24.1\",\n    \"cytoscape\": \"^3.31.1\",\n    \"cytoscape-canvas\": \"^3.0.1\",\n    \"cytoscape-cola\": \"^2.5.1\",\n    \"human-format\": \"^0.11.0\",\n    \"react\": \"^18.3.1\",\n    \"react-autosuggest\": \"^10.1.0\",\n    \"react-bootstrap-table-next\": \"^4.0.3\",\n    \"react-cytoscapejs\": \"^2.0.0\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-native\": \"^0.78.1\"\n  },\n  \"packageManager\": \"yarn@1.22.22\",\n  \"resolutions\": {\n    \"underscore\": \"^1.12.1\",\n    \"semver\": \"^6.3.1\"\n  }\n}\n"
  },
  {
    "path": "provisioning/dashboards/dashboards.yml",
    "content": "apiVersion: 1\n\nproviders:\n  - name: 'default'\n    orgId: 1\n    folder: ''\n    type: file\n    disableDeletion: true\n    allowUiUpdates: true\n    updateIntervalSeconds: 10 #how often Grafana will scan for changed dashboards\n    options:\n      path: /usr/share/grafana/custom/dashboards\n"
  },
  {
    "path": "provisioning/home/home.json",
    "content": "{\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"grafana\",\n          \"uid\": \"-- Grafana --\"\n        },\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 0,\n  \"id\": 1,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"datasource\": {\n        \"type\": \"datasource\",\n        \"uid\": \"grafana\"\n      },\n      \"gridPos\": {\n        \"h\": 17,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 1,\n      \"options\": {\n        \"dataMapping\": {\n          \"aggregationType\": \"service\",\n          \"baselineRtUpper\": \"threshold\",\n          \"errorRateColumn\": \"error_in\",\n          \"errorRateOutgoingColumn\": \"error_out\",\n          \"extOrigin\": \"origin_external\",\n          \"extTarget\": \"target_external\",\n          \"requestRateColumn\": \"in_count\",\n          \"requestRateOutgoingColumn\": \"out_count\",\n          \"responseTimeColumn\": \"in_timesum\",\n          \"responseTimeOutgoingColumn\": \"out_timesum\",\n          \"showDummyData\": true,\n          \"sourceColumn\": \"origin_service\",\n          \"targetColumn\": \"target_service\",\n          \"type\": \"protocol\"\n        },\n        \"drillDownLink\": \"\",\n        \"externalIcons\": [\n          {\n            \"filename\": \"web\",\n            \"pattern\": \"web\"\n          },\n          {\n            \"filename\": \"message\",\n            \"pattern\": \"jms\"\n          },\n          {\n            \"filename\": \"database\",\n            \"pattern\": \"jdbc\"\n          },\n          {\n            \"filename\": \"http\",\n            \"pattern\": \"http\"\n          }\n        ],\n        \"filterEmptyConnections\": true,\n        \"icons\": [\n          {\n            \"filename\": \"java\",\n            \"pattern\": \"java\"\n          },\n          {\n            \"filename\": \"star_trek\",\n            \"pattern\": \"spok|star trek\"\n          }\n        ],\n        \"showBaselines\": false,\n        \"showConnectionStats\": true,\n        \"showDebugInformation\": false,\n        \"style\": {\n          \"dangerColor\": \"rgb(196, 22, 42)\",\n          \"healthyColor\": \"rgb(87, 148, 242)\",\n          \"noDataColor\": \"rgb(123, 123, 138)\"\n        },\n        \"sumTimings\": true,\n        \"timeFormat\": \"m\"\n      },\n      \"pluginVersion\": \"4.2.0\",\n      \"title\": \"SDG\",\n      \"type\": \"novatec-sdg-panel\"\n    },\n    {\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 17\n      },\n      \"id\": 123125,\n      \"type\": \"gettingstarted\"\n    },\n    {\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 26\n      },\n      \"id\": 123124,\n      \"type\": \"gettingstarted\"\n    },\n    {\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 35\n      },\n      \"id\": 123123,\n      \"type\": \"gettingstarted\"\n    }\n  ],\n  \"schemaVersion\": 39,\n  \"tags\": [],\n  \"templating\": {\n    \"list\": []\n  },\n  \"time\": {\n    \"from\": \"now-1h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {},\n  \"timezone\": \"browser\",\n  \"title\": \"Dummy Dashboard\",\n  \"version\": 1,\n  \"weekStart\": \"\"\n}\n"
  },
  {
    "path": "scripts/create-signed-plugin.sh",
    "content": "#!/bin/bash\n\necho Building plugin...\nyarn install && yarn build\n\necho Signing plugin...\nyarn sign\n\necho Creating zip file...\ncp -r dist novatec-sdg-panel\nmkdir -p release && zip -r release/novatec-sdg-panel.zip novatec-sdg-panel\nrm -r novatec-sdg-panel\n"
  },
  {
    "path": "src/assets/icons/icon_index.json",
    "content": "[\"java\", \"star_trek\", \"balancer\", \"database\", \"default\", \"ftp\", \"http\", \"ldap\", \"mainframe\", \"message\", \"smtp\", \"web\"]\n"
  },
  {
    "path": "src/css/novatec-service-dependency-graph-panel.css",
    "content": ".service-dependency-graph-panel {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-direction: column;\n}\n\n.service-dependency-graph-panel .graph-container {\n  width: 100%;\n  height: 100%;\n  display: flex;\n  flex-direction: row;\n}\n\n.service-dependency-graph-panel .service-dependency-graph {\n  position: relative;\n  flex-grow: 1;\n  min-width: 0;\n}\n\n.service-dependency-graph-panel .canvas-container {\n   width: 100%;\n   height: 100%;\n   overflow: hidden;\n}\n\n.service-dependency-graph-panel .zoom-button-container {\n  position: absolute;\n  top: 0;\n  right: 1rem;\n  z-index: 99;\n  width: 35px;\n}\n\n.service-dependency-graph-panel .statistics {\n  flex-basis: 0;\n  transition: flex-basis 250ms ease-in-out;\n  overflow-y: scroll;\n\n}\n\n.service-dependency-graph-panel .statistics.show {\n  flex-basis: 30rem;\n  padding-left: 0.5%;\n}\n\n.service-dependency-graph-panel .header--selection {\n  font-size: 1.25em;\n  text-align: center;\n  border-bottom: 2px solid #161719;\n  font-weight: 500;\n  color: rgb(216, 217, 218);\n}\n\n.service-dependency-graph-panel .secondHeader--selection {\n  font-size: 1.2em;\n  text-align: center;\n  padding-top: 1.5rem;\n  padding-bottom: 0.5rem;\n}\n\n.service-dependency-graph-panel .no-data--selection{\n  color: #888888;\n  text-align: center;\n}\n\n.service-dependency-graph-panel .table--selection {\n  width: 99%;\n  table-layout: fixed;\n}\n\n.service-dependency-graph-panel .table--selection th, .table--selection td {\n  padding: 3px 5px;\n}\n\n.service-dependency-graph-panel .table--selection tr {\n  border-bottom: 2px solid #161719;\n}\n\n.service-dependency-graph-panel .table--td--selection {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.service-dependency-graph-panel .threshold--bad {\n  color:#f2495c;\n}\n\n.service-dependency-graph-panel .threshold--good {\n  color: #73bf69;\n}\n\n.service-dependency-graph-panel .table--th--selectionSmall {\n  width: 5.5rem;\n}\n\n.service-dependency-graph-panel .table--th--selectionMedium {\n  width: 8rem;\n}\n\n.service-dependency-graph-panel .table--selection--head {\n  background-color: #28282a;\n  border-top: 2px solid #161719;\n  color: #33b5e5;\n}\n\n.service-dependency-graph-panel .width-100 {\n  width: 100%;\n}\n"
  },
  {
    "path": "src/dummy_data_frame.ts",
    "content": "import { ArrayVector, DataFrame, FieldType } from '@grafana/data';\n\nconst data: DataFrame[] = [\n  {\n    refId: 'A',\n    name: undefined,\n    meta: undefined,\n    fields: [\n      {\n        name: 'time',\n        type: FieldType.time,\n        config: {},\n        values: new ArrayVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),\n      },\n      {\n        name: 'origin_external',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          '',\n          '',\n          '',\n          '',\n          '',\n          '',\n          '',\n          '',\n          '',\n          '',\n          '',\n          'tcp://localhost:61616',\n          'tcp://10.10.10.10:61616',\n        ]),\n      },\n      {\n        name: 'origin_service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          '',\n          '',\n          '',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'customers-service',\n          'vets-service',\n          'visits-service',\n          'vets-service',\n          '',\n        ]),\n      },\n      {\n        name: 'protocol',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'jms',\n          'jms',\n        ]),\n      },\n      {\n        name: 'service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'api-gateway',\n          'config-server',\n          'discovery-server',\n          'api-gateway',\n          'customers-service',\n          'discovery-server',\n          'vets-service',\n          'visits-service',\n          'discovery-server',\n          'discovery-server',\n          'discovery-server',\n          'visits-service',\n          'api-gateway',\n        ]),\n      },\n      {\n        name: 'namespace',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'demo.infrastructure',\n          'demo.infrastructure',\n          'demo.infrastructure',\n          'demo.infrastructure',\n          'demo.domain-logic',\n          'demo.infrastructure',\n          'demo.domain-logic',\n          'demo.domain-logic',\n          'demo.infrastructure',\n          'demo.infrastructure',\n          'demo.infrastructure',\n          'demo.domain-logic',\n          'demo.infrastructure',\n        ]),\n      },\n      {\n        name: 'in_count',\n        type: FieldType.number,\n        config: {},\n        values: new ArrayVector([508, 0, 0, 100, 347, 20, 63, 100, 20, 20, 20, 300, 300]),\n      },\n    ],\n    length: 13,\n  },\n  {\n    refId: 'B',\n    name: undefined,\n    meta: undefined,\n    fields: [\n      {\n        name: 'time',\n        type: FieldType.time,\n        config: {},\n        values: new ArrayVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),\n      },\n      {\n        name: 'protocol',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'jms',\n          'http',\n          'jdbc',\n          'jdbc',\n          'jdbc',\n        ]),\n      },\n      {\n        name: 'service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'config-server',\n          'customers-service',\n          'vets-service',\n          'vets-service',\n          'visits-service',\n          'customers-service',\n          'vets-service',\n          'visits-service',\n        ]),\n      },\n      {\n        name: 'namespace',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          '',\n          '',\n          '',\n          '',\n          '',\n          'web',\n          'web',\n          '',\n          '',\n          '',\n          '',\n          'demo.database',\n          'demo.database',\n          'demo.database',\n        ]),\n      },\n      {\n        name: 'target_external',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          '',\n          '',\n          '',\n          '',\n          '',\n          '7a8dce897616:8080',\n          'github.com',\n          '',\n          '',\n          'tcp://localhost:61616',\n          '',\n          'jdbc:hsqldb:mem:testdb',\n          'jdbc:hsqldb:mem:testdb',\n          'jdbc:hsqldb:mem:testdb',\n        ]),\n      },\n      {\n        name: 'target_service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'api-gateway',\n          'customers-service',\n          'discovery-server',\n          'vets-service',\n          'visits-service',\n          '',\n          '',\n          'discovery-server',\n          'discovery-server',\n          'visits-service',\n          'discovery-server',\n          '',\n          '',\n          '',\n        ]),\n      },\n      {\n        name: 'out_count',\n        type: FieldType.number,\n        config: {},\n        values: new ArrayVector([100, 347, 20, 62, 100, 0, 0, 20, 20, 300, 20, 1847, 441, 100]),\n      },\n    ],\n    length: 14,\n  },\n  {\n    refId: 'C',\n    name: undefined,\n    meta: undefined,\n    fields: [\n      { name: 'time', type: FieldType.time, config: {}, values: new ArrayVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) },\n      {\n        name: 'origin_service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          '',\n          '',\n          '',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'customers-service',\n          'vets-service',\n          'visits-service',\n        ]),\n      },\n      {\n        name: 'protocol',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n        ]),\n      },\n      {\n        name: 'service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'api-gateway',\n          'config-server',\n          'discovery-server',\n          'api-gateway',\n          'customers-service',\n          'discovery-server',\n          'vets-service',\n          'visits-service',\n          'discovery-server',\n          'discovery-server',\n          'discovery-server',\n        ]),\n      },\n      {\n        name: 'namespace',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'demo.infrastructure',\n          'demo.infrastructure',\n          'demo.infrastructure',\n          'demo.infrastructure',\n          'demo.domain-logic',\n          'demo.infrastructure',\n          'demo.domain-logic',\n          'demo.domain-logic',\n          'demo.domain-logic',\n          'demo.domain-logic',\n          'demo.infrastructure',\n          'demo.infrastructure',\n          'demo.infrastructure',\n        ]),\n      },\n      {\n        name: 'target_external',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector(['', '', '', '', '', '', '', '', '', '', '']),\n      },\n      {\n        name: 'in_timesum',\n        type: FieldType.number,\n        config: {},\n        values: new ArrayVector([\n          45140.008427999986, 0, 0, 1511.9842349999872, 819.3634589999965, 21.881731999999943, 281.0465210000002,\n          325.85070300000007, 21.53124399999996, 21.40604300000001, 20.813048000000038,\n        ]),\n      },\n    ],\n    length: 11,\n  },\n  {\n    refId: 'D',\n    name: undefined,\n    meta: undefined,\n    fields: [\n      {\n        name: 'time',\n        type: FieldType.time,\n        config: {},\n        values: new ArrayVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),\n      },\n      {\n        name: 'protocol',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'http',\n          'jdbc',\n          'jdbc',\n          'jdbc',\n        ]),\n      },\n      {\n        name: 'service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'api-gateway',\n          'config-server',\n          'customers-service',\n          'vets-service',\n          'visits-service',\n          'customers-service',\n          'vets-service',\n          'visits-service',\n        ]),\n      },\n      {\n        name: 'namespace',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          '',\n          '',\n          '',\n          '',\n          '',\n          'web',\n          'web',\n          '',\n          '',\n          '',\n          'demo.database',\n          'demo.database',\n          'demo.database',\n        ]),\n      },\n      {\n        name: 'target_external',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          '',\n          '',\n          '',\n          '',\n          '',\n          '7a8dce897616:8080',\n          'github.com',\n          '',\n          '',\n          '',\n          'jdbc:hsqldb:mem:testdb',\n          'jdbc:hsqldb:mem:testdb',\n          'jdbc:hsqldb:mem:testdb',\n        ]),\n      },\n      {\n        name: 'target_service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'api-gateway',\n          'customers-service',\n          'discovery-server',\n          'vets-service',\n          'visits-service',\n          '',\n          '',\n          'discovery-server',\n          'discovery-server',\n          'discovery-server',\n          '',\n          '',\n          '',\n        ]),\n      },\n      {\n        name: 'out_timesum',\n        type: FieldType.number,\n        config: {},\n        values: new ArrayVector([\n          1700.468872999987, 1481.533606999972, 540.746261, 501.65547400000014, 394.81158100000175, 0, 0,\n          84.59527999999978, 381.87400800000023, 225.65933600000017, 35.9093940000007, 13.000189000000091,\n          12.258137999999946,\n        ]),\n      },\n    ],\n    length: 13,\n  },\n  {\n    refId: 'E',\n    name: undefined,\n    meta: undefined,\n    fields: [\n      { name: 'time', type: FieldType.time, config: {}, values: new ArrayVector([0, 0, 0, 0]) },\n      { name: 'origin_service', type: FieldType.string, config: {}, values: new ArrayVector(['', '', '', '']) },\n      {\n        name: 'protocol',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector(['http', 'http', 'http', 'http']),\n      },\n      {\n        name: 'service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector(['api-gateway', 'discovery-server', 'customers-service', 'vets-service']),\n      },\n      {\n        name: 'namespace',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector([\n          'demo.infrastructure',\n          'demo.infrastructure',\n          'demo.domain-logic',\n          'demo.domain-logic',\n        ]),\n      },\n      { name: 'target_external', type: FieldType.string, config: {}, values: new ArrayVector(['', '', '', '']) },\n      { name: 'error_in', type: FieldType.number, config: {}, values: new ArrayVector([14, 20, 20, 0]) },\n    ],\n    length: 4,\n  },\n  {\n    refId: 'F',\n    name: undefined,\n    meta: undefined,\n    fields: [\n      { name: 'time', type: FieldType.time, config: {}, values: new ArrayVector([0, 0, 0, 0]) },\n      {\n        name: 'origin_service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector(['api-gateway', 'api-gateway', 'api-gateway', 'customers-service']),\n      },\n      {\n        name: 'namespace',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector(['demo.domain-logic', 'demo.domain-logic', 'demo.domain-logic', 'demo.infrastructure']),\n      },\n      {\n        name: 'protocol',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector(['http', 'http', 'http', 'http']),\n      },\n      {\n        name: 'service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector(['customers-service', 'vets-service', 'visits-service', 'discovery-server']),\n      },\n      { name: 'target_external', type: FieldType.string, config: {}, values: new ArrayVector(['', '', '', '']) },\n      { name: 'error_out', type: FieldType.number, config: {}, values: new ArrayVector([14, 0, 0, 20]) },\n    ],\n    length: 4,\n  },\n  {\n    refId: 'G',\n    name: undefined,\n    meta: undefined,\n    fields: [\n      { name: 'time', type: FieldType.time, config: {}, values: new ArrayVector([0, 0]) },\n      {\n        name: 'service',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector(['api-gateway', 'customers-service']),\n      },\n      {\n        name: 'namespace',\n        type: FieldType.string,\n        config: {},\n        values: new ArrayVector(['demo.infrastructure', 'demo.domain-logic']),\n      },\n      { name: 'threshold', type: FieldType.number, config: {}, values: new ArrayVector([40.40604300000001, 10]) },\n    ],\n    length: 2,\n  },\n];\n\nexport default data;\n"
  },
  {
    "path": "src/migration/PanelMigration.tsx",
    "content": "import { PanelModel } from '@grafana/data';\nimport { DefaultSettings } from 'options/DefaultSettings';\nimport { PanelSettings } from 'types';\n\n/**\n * Checks if the given options are in the format of version < 4.0.0.\n * @param options The options object which should be checked.\n */\nfunction isLegacyFormat(options: any) {\n  return options && !('showDummyData' in options['dataMapping']);\n}\n\n/**\n * Migrates the legacy iconMapping format to the iconMapping format of version > 4.0.0.\n * @param iconMappings The iconMappings object to be migrated.\n */\nfunction migrateIconMapping(iconMappings: any) {\n  const migratedIconMapping = [];\n  for (const iconMapping of iconMappings) {\n    migratedIconMapping.push({\n      pattern: iconMapping.name,\n      filename: iconMapping.filename,\n    });\n  }\n  return migratedIconMapping;\n}\n\n/**\n * Migrates the legacy panel settings from version < 4.0.0 to the new format introduced in version 4.0.0\n * The newly introduced variable aggregationType will be set to $aggregationTyoe in order to ensure functionality with\n * the legacy setup of the panel.\n * All other newly added options will be set to their respective default values.\n * @param panel The panel object which should be migrated.\n */\nexport const PanelMigrationHandler = (panel: PanelModel<Partial<PanelSettings>> | any) => {\n  const { settings } = panel;\n  if (isLegacyFormat(settings)) {\n    return {\n      animate: settings.animate,\n      sumTimings: settings.sumTimings,\n      filterEmptyConnections: settings.filterEmptyConnections,\n      style: {\n        healthyColor: settings.style.healthyColor,\n        dangerColor: settings.style.dangerColor,\n        noDataColor: settings.style.unknownColor,\n      },\n      showDebugInformation: settings.showDebugInformation,\n      showConnectionStats: settings.showConnectionStats,\n      externalIcons: migrateIconMapping(settings.externalIcons),\n      icons: settings.serviceIcons,\n      dataMapping: {\n        aggregationType: '$aggregationType',\n        sourceColumn: settings.dataMapping.sourceComponentPrefix + '$aggregationType',\n        targetColumn: settings.dataMapping.targetComponentPrefix + '$aggregationType',\n\n        responseTimeColumn: settings.dataMapping.responseTimeColumn,\n        requestRateColumn: settings.dataMapping.requestRateColumn,\n        errorRateColumn: settings.dataMapping.errorRateColumn,\n        responseTimeOutgoingColumn: settings.dataMapping.responseTimeOutgoingColumn,\n        requestRateOutgoingColumn: settings.dataMapping.requestRateOutgoingColumn,\n        errorRateOutgoingColumn: settings.dataMapping.errorRateOutgoingColumn,\n\n        extOrigin: settings.dataMapping.extOrigin,\n        extTarget: settings.dataMapping.extTarget,\n        type: settings.dataMapping.type,\n        showDummyData: settings.showDummyData,\n\n        baselineRtUpper: settings.dataMapping.baselineRtUpper,\n      },\n      drillDownLink: settings.drillDownLink,\n      showBaselines: settings.showBaselines,\n      timeFormat: DefaultSettings.timeFormat,\n    };\n  }\n  return settings;\n};\n"
  },
  {
    "path": "src/module.test.ts",
    "content": "// Just a stub test\ndescribe('placeholder test', () => {\n  it('should return true', () => {\n    expect(true).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "src/module.ts",
    "content": "import { PanelPlugin } from '@grafana/data';\nimport { PanelSettings } from './types';\n\nimport { PanelController } from './panel/PanelController';\nimport { optionsBuilder } from './options/options';\nimport { PanelMigrationHandler } from './migration/PanelMigration';\n\nexport const plugin = new PanelPlugin<PanelSettings>(PanelController)\n  .setPanelOptions(optionsBuilder)\n  .setMigrationHandler(PanelMigrationHandler);\n"
  },
  {
    "path": "src/options/DefaultSettings.tsx",
    "content": "import { PanelSettings } from '../types';\n\nexport const DefaultSettings: PanelSettings = {\n  animate: true,\n\n  dataMapping: {\n    aggregationType: 'service',\n    sourceColumn: 'origin_service',\n    targetColumn: 'target_service',\n    namespaceColumn: 'namespace',\n    namespaceDelimiter: '.',\n\n    responseTimeColumn: 'response-time',\n    requestRateColumn: 'request-rate',\n    errorRateColumn: 'error-rate',\n    responseTimeOutgoingColumn: 'response-time-out',\n    requestRateOutgoingColumn: 'request-rate-out',\n    errorRateOutgoingColumn: 'error-rate-out',\n\n    extOrigin: 'external_origin',\n    extTarget: 'external_target',\n    type: 'type',\n\n    baselineRtUpper: 'threshold',\n\n    showDummyData: false,\n  },\n\n  sumTimings: true,\n  filterEmptyConnections: true,\n  showDebugInformation: false,\n  showConnectionStats: true,\n  showBaselines: false,\n\n  style: {\n    healthyColor: 'rgb(87, 148, 242)',\n    dangerColor: 'rgb(196, 22, 42)',\n    noDataColor: 'rgb(123, 123, 138)',\n  },\n\n  icons: [\n    {\n      pattern: 'java',\n      filename: 'java',\n    },\n    {\n      pattern: 'spok|star trek',\n      filename: 'star_trek',\n    },\n  ],\n\n  externalIcons: [\n    {\n      pattern: 'web',\n      filename: 'web',\n    },\n    {\n      pattern: 'jms',\n      filename: 'message',\n    },\n    {\n      pattern: 'jdbc',\n      filename: 'database',\n    },\n    {\n      pattern: 'http',\n      filename: 'http',\n    },\n  ],\n\n  drillDownLink: '',\n  timeFormat: 'm',\n};\n"
  },
  {
    "path": "src/options/TypeAheadTextfield/TypeaheadTextfield.css",
    "content": ".service-dependency-graph-panel .suggestion {\n    width: 100%;\n    border-right: 1px solid #3865AB ;\n    border-left: 1px solid #3865AB ;\n    background-color: #0B0C0E;\n    padding-left: 10px;\n}\n\n.service-dependency-graph-panel ul {\n    list-style-type: none;\n}\n\n.service-dependency-graph-panel ul:last-child {\n    border-bottom: 1px solid #3865AB ;\n}\n"
  },
  {
    "path": "src/options/TypeAheadTextfield/TypeaheadTextfield.tsx",
    "content": "import React from 'react';\nimport Autosuggest, { InputProps } from 'react-autosuggest';\nimport { StandardEditorContext, StandardEditorProps } from '@grafana/data';\nimport './TypeaheadTextfield.css';\nimport { PanelSettings } from '../../types';\ninterface Props extends StandardEditorProps<string, PanelSettings> {\n  item: any;\n  value: string;\n  onChange: (value?: string) => void;\n  context: StandardEditorContext<any>;\n}\ninterface State {\n  item: any;\n  value: string;\n  onChange: (value?: string) => void;\n  context: StandardEditorContext<any>;\n  suggestions: string[];\n}\nexport class TypeaheadTextField extends React.PureComponent<Props, State> {\n  constructor(props: Props | Readonly<Props>) {\n    super(props);\n    let { value } = props;\n    if (value === undefined) {\n      value = props.item.defaultValue;\n    }\n    this.state = {\n      ...props,\n      value: value,\n      suggestions: [],\n    };\n  }\n  renderSuggestion(suggestion: string) {\n    return <div>{suggestion}</div>;\n  }\n  getColumnNames() {\n    let { data } = this.props.context;\n    let series;\n    let columnNames = [];\n    if (data !== undefined && data.length > 0) {\n      series = data[0].fields;\n      for (const index in series) {\n        const field = series[index];\n        const { config, name } = field;\n        if (config !== undefined && config.displayName !== undefined) {\n          columnNames.push(config.displayName);\n        } else {\n          columnNames.push(name);\n        }\n      }\n    }\n    return columnNames;\n  }\n  onChange = (event: React.FormEvent<HTMLElement>, { newValue }: { newValue: string }) => {\n    //TODO make this type nicer!\n    const { path } = this.props.item;\n    const { value } = event.currentTarget as HTMLInputElement;\n    this.setState({\n      value: value,\n    });\n    this.props.onChange.call(path, newValue);\n  };\n  getSuggestions = (value: string) => {\n    let inputValue = '';\n    if (value !== undefined) {\n      return [];\n    }\n    if (value !== undefined && value !== null && value !== '') {\n      inputValue = value.trim().toLowerCase();\n    }\n    const inputLength = inputValue.length;\n    if (inputLength === 0 || inputValue === undefined) {\n      return [];\n    }\n    return this.getColumnNames().filter((columnName) => columnName.toLowerCase().startsWith(inputValue));\n  };\n  onSuggestionsFetchRequested = (value: any) => {\n    this.setState({\n      suggestions: this.getSuggestions(value),\n    });\n  };\n  getSuggestionValue = (suggestion: string) => {\n    return suggestion;\n  };\n  onSuggestionsClearRequested = () => {\n    this.setState({\n      suggestions: [],\n    });\n  };\n  render() {\n    let { value } = this.props;\n    if (value === undefined) {\n      value = this.props.item.defaultValue;\n    }\n    const suggestions = this.getSuggestions(value);\n    const inputProps: InputProps<string> = {\n      placeholder: 'Enter column name...',\n      value,\n      onChange: this.onChange,\n    };\n    return (\n      <Autosuggest\n        suggestions={suggestions}\n        onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}\n        onSuggestionsClearRequested={this.onSuggestionsClearRequested}\n        getSuggestionValue={this.getSuggestionValue}\n        renderSuggestion={this.renderSuggestion}\n        inputProps={inputProps}\n        theme={{\n          input: 'input-small gf-form-input width-100',\n          suggestion: 'suggestion',\n        }}\n      />\n    );\n  }\n}\n"
  },
  {
    "path": "src/options/dummyDataSwitch/DummyDataSwitch.tsx",
    "content": "import React from 'react';\nimport { StandardEditorContext, StandardEditorProps } from '@grafana/data';\nimport { PanelSettings, DataMapping } from '../../types';\nimport { Switch } from '@grafana/ui';\n\ninterface Props extends StandardEditorProps<boolean, PanelSettings> {\n  item: any;\n  value: boolean;\n  onChange: (value?: boolean) => void;\n  context: StandardEditorContext<any>;\n}\n\ninterface State {\n  item: any;\n  value: boolean;\n  dataMapping: DataMapping | undefined;\n  onChange: (value?: boolean) => void;\n  context: StandardEditorContext<any>;\n}\n\nexport class DummyDataSwitch extends React.PureComponent<Props, State> {\n  constructor(props: Props | Readonly<Props>) {\n    super(props);\n\n    let { dataMapping } = props.context.options;\n    if (dataMapping === undefined) {\n      dataMapping = props.item.defaultValue;\n    }\n    this.state = {\n      dataMapping: dataMapping,\n      ...props,\n    };\n  }\n\n  getDummyDataMapping = () => {\n    return {\n      aggregationType: 'service',\n      sourceColumn: 'origin_service',\n      targetColumn: 'target_service',\n      responseTimeColumn: 'in_timesum',\n      requestRateColumn: 'in_count',\n      errorRateColumn: 'error_in',\n      responseTimeOutgoingColumn: 'out_timesum',\n      requestRateOutgoingColumn: 'out_count',\n      errorRateOutgoingColumn: 'error_out',\n      extOrigin: 'origin_external',\n      extTarget: 'target_external',\n      type: 'protocol',\n      showDummyData: true,\n      baselineRtUpper: 'threshold',\n    };\n  };\n\n  onChange = () => {\n    let { dataMapping } = this.props.context.options;\n    const { item } = this.state;\n    const { onChange } = this.props;\n    const newValue = !dataMapping.showDummyData;\n\n    if (newValue) {\n      this.setState({ dataMapping: dataMapping });\n      dataMapping = this.getDummyDataMapping();\n    }\n    dataMapping.showDummyData = newValue;\n    onChange.call(item.path, dataMapping);\n  };\n\n  render() {\n    let { dataMapping } = this.props.context.options;\n    if (dataMapping === undefined) {\n      dataMapping = this.props.item.defaultValue;\n      this.props.context.options.dataMapping = this.props.item.defaultValue;\n    }\n\n    return (\n      <div>\n        <Switch value={dataMapping.showDummyData} onChange={() => this.onChange()} />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "src/options/iconMapping/IconMapping.css",
    "content": ".service-dependency-graph-panel .no-background {\n    background-color: transparent;\n}\n\n.service-dependency-graph-panel .no-padding-left {\n    padding-left: 0;\n}\n\n.service-dependency-graph-panel .icon-mapping-button {\n    width: 46.5%;\n    margin-left: 0;\n}\n\n.service-dependency-graph-panel .width-100 {\n    width: 100%;\n}\n\n.service-dependency-graph-panel .width-half {\n    width: 47%;\n    text-overflow: ellipsis;\n}\n"
  },
  {
    "path": "src/options/iconMapping/IconMapping.tsx",
    "content": "import React, { ChangeEvent } from 'react';\nimport { StandardEditorContext, StandardEditorProps } from '@grafana/data';\nimport { IconResource, PanelSettings } from '../../types';\nimport assetUtils from '../../panel/asset_utils';\nimport './IconMapping.css';\n\ninterface Props extends StandardEditorProps<string, PanelSettings> {\n  item: any;\n  value: any;\n  onChange: (value?: string) => void;\n  context: StandardEditorContext<any>;\n}\n\ninterface State {\n  item: any;\n  value: string;\n  onChange: (value?: string) => void;\n  context: StandardEditorContext<any>;\n  icons: string[];\n}\n\nexport class IconMapping extends React.PureComponent<Props, State> {\n  constructor(props: Props | Readonly<Props>) {\n    super(props);\n    this.state = {\n      ...props,\n      icons: [],\n    };\n    fetch(assetUtils.getAssetUrl('icon_index.json'))\n      .then((response) => response.json())\n      .then((data) => {\n        data.sort();\n        this.setState({\n          icons: data,\n        });\n      })\n      .catch(() => {\n        console.error(\n          'Could not load service icons mapping index. Please verify the \"icon_index.json\" in the plugin\\'s asset directory.'\n        );\n      });\n  }\n\n  addMapping() {\n    const { path } = this.state.item;\n    const icons = this.state.context.options[path];\n    icons.push({ pattern: 'my-type', filename: 'default' });\n    this.state.onChange.call(path, icons);\n  }\n\n  removeMapping(index: number) {\n    const { path } = this.state.item;\n    const icons = this.state.context.options[path];\n    icons.splice(index, 1);\n    this.state.onChange.call(path, icons);\n  }\n\n  setPatternValue(event: React.ChangeEvent<HTMLInputElement>, index: number) {\n    const { path } = this.state.item;\n    const icons = this.state.context.options[path];\n    icons[index].pattern = event.currentTarget.value;\n    this.state.onChange.call(path, icons);\n  }\n\n  setFileNameValue(event: ChangeEvent<HTMLSelectElement>, index: number) {\n    const { path } = this.state.item;\n    const icons = this.state.context.options[path];\n    icons[index].filename = event.currentTarget.value.toString();\n    this.props.onChange.call(path, icons);\n  }\n\n  render() {\n    const { path } = this.state.item;\n    const { icons: iconNames } = this.state;\n    let icons = this.state.context.options[path];\n    if (icons === undefined) {\n      icons = this.state.item.defaultValue;\n      const context = this.state.context;\n      context.options[path] = this.state.item.defaultValue;\n      this.setState({\n        context: context,\n      });\n    }\n\n    return (\n      <div>\n        <div className=\"gf-form-inline\">\n          <div className=\"gf-form width-100\">\n            <label className=\"gf-form-label no-background no-padding-left width-half\">Target Name (RegEx)</label>\n            <label className=\"gf-form-label no-background no-padding-left width-half\">Icon</label>\n          </div>\n        </div>\n        <div>\n          {icons.map((icon: IconResource, index: number) => (\n            <>\n              <div className=\"gf-form\">\n                <input\n                  type=\"text\"\n                  className=\"input-small gf-form-input\"\n                  value={icon.pattern}\n                  onChange={(e) => this.setPatternValue(e, index)}\n                />\n\n                <select\n                  className=\"input-small gf-form-input\"\n                  value={icon.filename}\n                  onChange={(e) => this.setFileNameValue(e, index)}\n                >\n                  {iconNames.map((iconName: string, index: number) => (\n                    <option key={iconName + '-' + index} value={iconName}>\n                      {iconName}\n                    </option>\n                  ))}\n                </select>\n\n                <a className=\"gf-form-label tight-form-func no-background\" onClick={() => this.removeMapping(index)}>\n                  <i className=\"fa fa-trash\"></i>\n                </a>\n              </div>\n            </>\n          ))}\n        </div>\n        <button\n          className=\"btn navbar-button navbar-button--primary icon-mapping-button\"\n          onClick={() => this.addMapping()}\n        >\n          Add Icon Mapping\n        </button>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "src/options/options.tsx",
    "content": "import { PanelOptionsEditorBuilder } from '@grafana/data';\nimport { PanelSettings } from '../types';\nimport { TypeaheadTextField } from './TypeAheadTextfield/TypeaheadTextfield';\nimport { IconMapping } from './iconMapping/IconMapping';\nimport { DummyDataSwitch } from './dummyDataSwitch/DummyDataSwitch';\nimport { DefaultSettings } from './DefaultSettings';\n\nexport const optionsBuilder = (builder: PanelOptionsEditorBuilder<PanelSettings>) => {\n  return (\n    builder\n\n      //Connection Mapping\n      .addCustomEditor({\n        path: 'dataMapping.aggregationType',\n        id: 'aggregationType',\n        editor: TypeaheadTextField,\n        name: 'Component Column',\n        category: ['Connection Mapping'],\n        defaultValue: DefaultSettings.dataMapping.aggregationType,\n      })\n\n      .addCustomEditor({\n        path: 'dataMapping.sourceColumn',\n        id: 'sourceComponentPrefix',\n        editor: TypeaheadTextField,\n        name: 'Source Component Column',\n        category: ['Connection Mapping'],\n        defaultValue: DefaultSettings.dataMapping.sourceColumn,\n      })\n\n      .addCustomEditor({\n        path: 'dataMapping.targetColumn',\n        id: 'targetComponentPrefix',\n        name: 'Target Component Column',\n        category: ['Connection Mapping'],\n        editor: TypeaheadTextField,\n        defaultValue: DefaultSettings.dataMapping.targetColumn,\n      })\n\n      .addCustomEditor({\n        path: 'dataMapping.namespaceColumn',\n        id: 'namespaceColumn',\n        name: 'Namespace Column',\n        category: ['Connection Mapping'],\n        editor: TypeaheadTextField,\n        defaultValue: DefaultSettings.dataMapping.namespaceColumn,\n      })\n\n      .addCustomEditor({\n        path: 'dataMapping.nameSpaceDelimiter',\n        id: 'nameSpaceDelimiter',\n        name: 'Namespace Delimiter',\n        category: ['Connection Mapping'],\n        editor: TypeaheadTextField,\n        defaultValue: DefaultSettings.dataMapping.namespaceDelimiter,\n      })\n\n      .addCustomEditor({\n        path: 'dataMapping.type',\n        id: 'type',\n        name: 'Type',\n        category: ['Connection Mapping'],\n        editor: TypeaheadTextField,\n        defaultValue: DefaultSettings.dataMapping.type,\n      })\n\n      .addCustomEditor({\n        path: 'dataMapping.extOrigin',\n        id: 'externalOrigin',\n        name: 'External Origin',\n        category: ['Connection Mapping'],\n        editor: TypeaheadTextField,\n        defaultValue: DefaultSettings.dataMapping.extOrigin,\n      })\n\n      .addCustomEditor({\n        path: 'dataMapping.extTarget',\n        id: 'externalTarget',\n        name: 'External Target',\n        category: ['Connection Mapping'],\n        editor: TypeaheadTextField,\n        defaultValue: DefaultSettings.dataMapping.extTarget,\n      })\n\n      //Data Mapping\n      .addCustomEditor({\n        id: 'responseTime',\n        path: 'dataMapping.responseTimeColumn',\n        name: 'Response Time Column',\n        editor: TypeaheadTextField,\n        category: ['Data Mapping'],\n        defaultValue: DefaultSettings.dataMapping.responseTimeColumn,\n      })\n\n      .addCustomEditor({\n        id: 'requestRateColumn',\n        path: 'dataMapping.requestRateColumn',\n        name: 'Request Rate Column',\n        editor: TypeaheadTextField,\n        category: ['Data Mapping'],\n        defaultValue: DefaultSettings.dataMapping.requestRateColumn,\n      })\n\n      .addCustomEditor({\n        id: 'errorRateColumn',\n        path: 'dataMapping.errorRateColumn',\n        name: 'Error Rate Column',\n        editor: TypeaheadTextField,\n        category: ['Data Mapping'],\n        defaultValue: DefaultSettings.dataMapping.errorRateColumn,\n      })\n\n      .addCustomEditor({\n        id: 'responseTimeOutgoingColumn',\n        path: 'dataMapping.responseTimeOutgoingColumn',\n        name: 'Response Time Column (Outgoing)',\n        editor: TypeaheadTextField,\n        category: ['Data Mapping'],\n        defaultValue: DefaultSettings.dataMapping.responseTimeOutgoingColumn,\n      })\n\n      .addCustomEditor({\n        id: 'requestRateOutgoingColumn',\n        path: 'dataMapping.requestRateOutgoingColumn',\n        name: 'Request Rate Column (Outgoing)',\n        editor: TypeaheadTextField,\n        category: ['Data Mapping'],\n        defaultValue: DefaultSettings.dataMapping.requestRateOutgoingColumn,\n      })\n\n      .addCustomEditor({\n        id: 'errorRateOutgoingColumn',\n        path: 'dataMapping.errorRateOutgoingColumn',\n        name: 'Error Rate Column (Outgoing)',\n        editor: TypeaheadTextField,\n        category: ['Data Mapping'],\n        defaultValue: DefaultSettings.dataMapping.errorRateOutgoingColumn,\n      })\n\n      .addCustomEditor({\n        id: 'baselineRtUpper',\n        path: 'dataMapping.baselineRtUpper',\n        name: 'Response Time Baseline (Upper)',\n        editor: TypeaheadTextField,\n        category: ['Data Mapping'],\n        defaultValue: DefaultSettings.dataMapping.baselineRtUpper,\n      })\n\n      //General Settings\n      .addBooleanSwitch({\n        path: 'showConnectionStats',\n        name: 'Show Connection Statistics',\n        category: ['General Settings'],\n        defaultValue: DefaultSettings.showConnectionStats,\n      })\n\n      .addBooleanSwitch({\n        path: 'sumTimings',\n        name: 'Handle Timings as Sums',\n        description:\n          'If this setting is active, the timings provided' +\n          'by the mapped response time columns are considered as a ' +\n          'continually increasing sum of response times. When ' +\n          'deactivated, it is considered that the timings provided ' +\n          'by columns are the actual average response times.',\n        category: ['General Settings'],\n        defaultValue: DefaultSettings.sumTimings,\n      })\n\n      .addBooleanSwitch({\n        path: 'filterEmptyConnections',\n        name: 'Filter Empty Data',\n        description:\n          'If this setting is active, the timings provided by ' +\n          'the mapped response time columns are considered as a continually ' +\n          'increasing sum of response times. When deactivated, it is considered ' +\n          'that the timings provided by columns are the actual average response times.',\n        category: ['General Settings'],\n        defaultValue: DefaultSettings.filterEmptyConnections,\n      })\n\n      .addBooleanSwitch({\n        path: 'showDebugInformation',\n        name: 'Show Debug Information',\n        category: ['General Settings'],\n        defaultValue: DefaultSettings.showDebugInformation,\n      })\n\n      .addCustomEditor({\n        path: 'dataMapping',\n        id: 'dummyDataSwitch',\n        name: 'Show Dummy Data',\n        editor: DummyDataSwitch,\n        category: ['General Settings'],\n        defaultValue: DefaultSettings.dataMapping,\n      })\n\n      .addBooleanSwitch({\n        path: 'showBaselines',\n        name: 'Show Baselines',\n        category: ['General Settings'],\n        defaultValue: DefaultSettings.showBaselines,\n      })\n\n      .addSelect({\n        path: 'timeFormat',\n        name: 'Maximum Time Unit to Resolve',\n        description:\n          'This setting controls to which time unit time values will be resolved to. ' +\n          'Each value always includes the smaller units.',\n        category: ['General Settings'],\n        settings: {\n          options: [\n            { value: 'ms', label: 'ms' },\n            { value: 's', label: 's' },\n            { value: 'm', label: 'm' },\n          ],\n        },\n        defaultValue: DefaultSettings.timeFormat,\n      })\n\n      //Appearance\n      .addColorPicker({\n        path: 'style.healthyColor',\n        name: 'Healthy Color',\n        category: ['Appearance'],\n        defaultValue: DefaultSettings.style.healthyColor,\n      })\n\n      .addColorPicker({\n        path: 'style.dangerColor',\n        name: 'Danger Color',\n        category: ['Appearance'],\n        defaultValue: DefaultSettings.style.dangerColor,\n      })\n\n      .addColorPicker({\n        path: 'style.noDataColor',\n        name: 'No Data Color',\n        category: ['Appearance'],\n        defaultValue: DefaultSettings.style.noDataColor,\n      })\n\n      //Icon Mapping\n      .addCustomEditor({\n        path: 'icons',\n        id: 'iconMapping',\n        editor: IconMapping,\n        name: '',\n        description:\n          'This setting controls which images should be mapped to your directly monitored nodes. ' +\n          'The node names are matched by the regex pattern provided in the \"Target Name(Regex) column.',\n        category: ['Icon Mapping'],\n        defaultValue: DefaultSettings.icons,\n      })\n\n      //External Icon Mapping\n      .addCustomEditor({\n        path: 'externalIcons',\n        id: 'externalIconMapping',\n        editor: IconMapping,\n        name: '',\n        description:\n          'This setting controls which images should be mapped to the external nodes. ' +\n          'The given type column is matched by the regex pattern provided in the \"Target Name(Regex) column.',\n        category: ['External Icon Mapping'],\n        defaultValue: DefaultSettings.externalIcons,\n      })\n\n      //Tracing Drilldown\n      .addTextInput({\n        path: 'drillDownLink',\n        name: 'Backend URL',\n        category: ['Tracing Drilldown'],\n        defaultValue: DefaultSettings.drillDownLink,\n      })\n  );\n};\n"
  },
  {
    "path": "src/panel/PanelController.tsx",
    "content": "import React, { LegacyRef, PureComponent } from 'react';\nimport {\n  AbsoluteTimeRange,\n  DataFrame,\n  FieldConfigSource,\n  InterpolateFunction,\n  PanelProps,\n  TimeRange,\n} from '@grafana/data';\nimport { ServiceDependencyGraph } from './serviceDependencyGraph/ServiceDependencyGraph';\nimport _ from 'lodash';\nimport { CurrentData, CyData, IntGraph, IntGraphEdge, IntGraphNode, PanelSettings } from '../types';\nimport cytoscape, { EdgeSingular, NodeSingular } from 'cytoscape';\nimport '../css/novatec-service-dependency-graph-panel.css';\nimport GraphGenerator from 'processing/graph_generator';\nimport PreProcessor from 'processing/pre_processor';\nimport data from '../dummy_data_frame';\nimport { getTemplateSrv } from '@grafana/runtime';\n\ninterface Props extends PanelProps<PanelSettings> {}\n\ninterface PanelState {\n  id: string | number;\n  fieldConfig: FieldConfigSource<any>;\n  height: number;\n  width: number;\n  onChangeTimeRange: (timeRange: AbsoluteTimeRange) => void;\n  onFieldConfigChange: (config: FieldConfigSource<any>) => void;\n  onOptionsChange: (options: PanelSettings) => void;\n  renderCounter: number;\n  replaceVariables: InterpolateFunction;\n  timeRange: TimeRange;\n  timeZone: string;\n  title: string;\n  transparent: boolean;\n  options: PanelSettings;\n  currentLayer: number;\n}\n\nexport class PanelController extends PureComponent<Props, PanelState> {\n  cy: cytoscape.Core | undefined;\n\n  ref: LegacyRef<HTMLDivElement>;\n\n  validQueryTypes: boolean;\n\n  graphGenerator: GraphGenerator;\n\n  preProcessor: PreProcessor;\n\n  currentData: CurrentData;\n\n  maxLayer = 0;\n\n  constructor(props: Props) {\n    super(props);\n    this.state = {\n      currentLayer: 0,\n      ...props,\n    };\n    this.ref = React.createRef();\n    this.graphGenerator = new GraphGenerator(this);\n    this.preProcessor = new PreProcessor(this);\n  }\n\n  getSettings(resolveVariables: boolean): PanelSettings {\n    if (resolveVariables) {\n      return this.resolveVariables(this.props.options);\n    }\n    return this.props.options;\n  }\n\n  resolveVariables(element: any) {\n    if (element instanceof Object) {\n      const newObject: any = {};\n      for (const key of Object.keys(element)) {\n        newObject[key] = this.resolveVariables(element[key]);\n      }\n      return newObject;\n    }\n\n    if (element instanceof String || typeof element === 'string') {\n      return getTemplateSrv().replace(element.toString());\n    }\n    return element;\n  }\n\n  resolveTemplateVars(input: any, copy: boolean) {\n    let value = input;\n    if (copy) {\n      value = _.cloneDeep(value);\n    }\n\n    if (typeof value === 'string' || value instanceof String) {\n      value = getTemplateSrv().replace(value.toString());\n    }\n    if (value instanceof Object) {\n      for (const key of Object.keys(value)) {\n        value[key] = this.resolveTemplateVars(value[key], false);\n      }\n    }\n    return value;\n  }\n\n  componentDidUpdate() {\n    this.processData();\n  }\n\n  processQueryData(data: DataFrame[]) {\n    this.validQueryTypes = this.hasOnlyTableQueries(data);\n    const graphData = this.preProcessor.processData(data);\n\n    this.currentData = graphData;\n  }\n\n  hasOnlyTableQueries(inputData: DataFrame[]) {\n    let result = true;\n\n    _.each(inputData, (dataElement) => {\n      if (!_.has(dataElement, 'columns')) {\n        result = false;\n      }\n    });\n\n    return result;\n  }\n\n  processData() {\n    let inputData: DataFrame[] = this.props.data.series;\n    if (this.getSettings(true).dataMapping.showDummyData) {\n      inputData = data;\n    }\n    this.processQueryData(inputData);\n    const graph: IntGraph = this.graphGenerator.generateGraph(this.currentData.graph);\n    return graph;\n  }\n\n  _transformEdges(edges: IntGraphEdge[]): CyData[] {\n    const cyEdges = _.map(edges, (edge) => {\n      const cyEdge = {\n        group: 'edges',\n        data: {\n          id: edge.source + ':' + edge.target,\n          source: edge.source,\n          target: edge.target,\n          metrics: {\n            ...edge.metrics,\n          },\n        },\n      };\n      return cyEdge;\n    });\n\n    return cyEdges;\n  }\n\n  _transformNodes(nodes: IntGraphNode[]): CyData[] {\n    const cyNodes = _.map(nodes, (node) => {\n      const result: CyData = {\n        group: 'nodes',\n        data: {\n          id: node.data.id,\n          type: node.data.type,\n          external_type: node.data.external_type,\n          namespace: node.data.namespace,\n          layer: node.data.layer,\n          parent: node.data.parent,\n          metrics: {\n            ...node.data.metrics,\n          },\n        },\n      };\n      return result;\n    });\n\n    return cyNodes;\n  }\n\n  _updateOrRemove(dataArray: Array<NodeSingular | EdgeSingular>, inputArray: CyData[]) {\n    const elements: Array<NodeSingular | EdgeSingular> = [];\n    for (let i = 0; i < dataArray.length; i++) {\n      const element = dataArray[i];\n\n      const cyNode = _.find(inputArray, { data: { id: element.id() } });\n\n      if (cyNode) {\n        element.data(cyNode.data);\n        _.remove(inputArray, (n) => n.data.id === cyNode.data.id);\n        elements.push(element);\n      } else {\n        element.remove();\n      }\n    }\n    return elements;\n  }\n\n  getError(): string | null {\n    if (!this.isDataAvailable()) {\n      return 'No data to show - the query returned no data.';\n    }\n    return null;\n  }\n\n  isDataAvailable() {\n    const dataExist =\n      !_.isUndefined(this.currentData) && !_.isUndefined(this.currentData.graph) && this.currentData.graph.length > 0;\n    return dataExist;\n  }\n\n  layer(layerIncrease: number) {\n    const that = this;\n    const currentLayer = that.state ? that.state.currentLayer : 0;\n    let layer = Math.max(0, currentLayer + layerIncrease);\n    if (layerIncrease > 0) {\n      layer = Math.min(that.maxLayer, currentLayer + layerIncrease);\n    }\n    that.setState({\n      currentLayer: layer,\n    });\n  }\n\n  render() {\n    const data = this.processData();\n    const error = this.getError();\n    if (error === null) {\n      // This is our root DOM\n      return (\n        <div>\n          <div\n            // The className is also used to scope all of our css rules\n            className=\"service-dependency-graph-panel\"\n            style={{ height: this.props.height, width: this.props.width }}\n            ref={this.ref}\n            id=\"cy\"\n          >\n            <ServiceDependencyGraph\n              data={data}\n              zoom={1}\n              maxLayer={this.maxLayer}\n              controller={this}\n              animate={false}\n              showStatistics={false}\n              settings={this.props.options}\n              layerIncreaseFunction={() => this.layer(+1)}\n              layerDecreaseFunction={() => this.layer(-1)}\n              layer={0}\n            />\n          </div>\n        </div>\n      );\n    } else {\n      return <div>{error}</div>;\n    }\n  }\n}\n"
  },
  {
    "path": "src/panel/asset_utils.tsx",
    "content": "import { find } from 'lodash';\nimport { IconResource } from 'types';\n\nexport default {\n  getAssetUrl(assetName: string) {\n    let baseUrl = 'public/plugins/novatec-sdg-panel';\n    return baseUrl + '/assets/icons/' + assetName;\n  },\n\n  getTypeSymbol(type: string, externalIcons: IconResource[], resolveName = true) {\n    if (!type) {\n      return this.getAssetUrl('default.png');\n    }\n\n    if (!resolveName) {\n      return this.getAssetUrl(type);\n    }\n\n    const icon = find(externalIcons, (icon) => icon.pattern.toLowerCase() === type.toLowerCase());\n\n    if (icon !== undefined) {\n      return this.getAssetUrl(icon.filename + '.png');\n    } else {\n      return this.getAssetUrl('default.png');\n    }\n  },\n};\n"
  },
  {
    "path": "src/panel/canvas/collision_detector.ts",
    "content": "import _ from 'lodash';\nimport { Point, Rectangle } from 'types';\n\nexport default class CollisionDetector {\n  blockedArea: Rectangle[];\n\n  constructor() {\n    this.blockedArea = [];\n  }\n\n  reset() {\n    this.blockedArea = [];\n  }\n\n  addRectangle(x: number, y: number, width: number, height: number) {\n    const rectangle: Rectangle = {\n      coordinates: {\n        x: x,\n        y: y,\n      },\n      height: height,\n      width: width,\n    };\n    this.blockedArea.push(rectangle);\n  }\n\n  isColliding(shape: Rectangle) {\n    const collidingShape = this.blockedArea.find((blockingShape) => {\n      if (this._intersects(shape, blockingShape)) {\n        return true;\n      }\n      return false;\n    });\n    return collidingShape !== undefined;\n  }\n\n  _intersects(a: Rectangle, b: Rectangle) {\n    const topLeft1: Point = a.coordinates;\n    const topLeft2: Point = b.coordinates;\n    const bottomRight1 = this._getBottomRightCorner(a);\n    const bottomRight2 = this._getBottomRightCorner(b);\n\n    if (topLeft1.x > bottomRight2.x || topLeft2.x > bottomRight1.x) {\n      return false;\n    }\n    if (topLeft1.y > bottomRight2.y || topLeft2.y > bottomRight1.y) {\n      return false;\n    }\n    return true;\n  }\n\n  _getBottomRightCorner(rectangle: Rectangle) {\n    const cornerPoint: Point = {\n      x: rectangle.coordinates.x + rectangle.width,\n      y: rectangle.coordinates.y + rectangle.height,\n    };\n    return cornerPoint;\n  }\n}\n"
  },
  {
    "path": "src/panel/canvas/graph_canvas.ts",
    "content": "import _ from 'lodash';\nimport cytoscape from 'cytoscape';\nimport { ServiceDependencyGraph } from '../serviceDependencyGraph/ServiceDependencyGraph';\nimport ParticleEngine from './particle_engine';\nimport {\n  CyCanvas,\n  Particle,\n  EnGraphNodeType,\n  Particles,\n  IntGraphMetrics,\n  ScaleValue,\n  DrawContext,\n  Rectangle,\n  Point,\n} from '../../types';\nimport humanFormat from 'human-format';\nimport assetUtils from '../asset_utils';\nimport CollisionDetector from './collision_detector';\n\nconst scaleValues: ScaleValue[] = [\n  { unit: 'ms', factor: 1 },\n  { unit: 's', factor: 1000 },\n  { unit: 'm', factor: 60000 },\n];\n\nexport default class CanvasDrawer {\n  readonly colors = {\n    default: '#bad5ed',\n    background: '#212121',\n    edge: '#505050',\n    status: {\n      warning: 'orange',\n    },\n  };\n\n  readonly donutRadius: number = 15;\n\n  controller: ServiceDependencyGraph;\n\n  cytoscape: cytoscape.Core;\n\n  context: CanvasRenderingContext2D;\n\n  cyCanvas: CyCanvas;\n\n  canvas: HTMLCanvasElement;\n\n  offscreenCanvas: HTMLCanvasElement;\n\n  offscreenContext: CanvasRenderingContext2D;\n\n  frameCounter = 0;\n\n  fpsCounter = 0;\n\n  particleImage: HTMLImageElement;\n\n  pixelRatio: number;\n\n  imageAssets: any = {};\n\n  selectionNeighborhood: cytoscape.Collection;\n\n  particleEngine: ParticleEngine;\n\n  collisionDetector: CollisionDetector;\n\n  lastRenderTime = 0;\n\n  dashAnimationOffset = 0;\n\n  constructor(ctrl: ServiceDependencyGraph, cy: cytoscape.Core, cyCanvas: CyCanvas) {\n    this.cytoscape = cy;\n    this.cyCanvas = cyCanvas;\n    this.controller = ctrl;\n    this.particleEngine = new ParticleEngine(this);\n    this.collisionDetector = new CollisionDetector();\n\n    this.pixelRatio = window.devicePixelRatio || 1;\n\n    this.canvas = cyCanvas.getCanvas();\n    const ctx = this.canvas.getContext('2d');\n    if (ctx) {\n      this.context = ctx;\n    } else {\n      console.error('Could not get 2d canvas context.');\n    }\n\n    this.offscreenCanvas = document.createElement('canvas');\n    this.offscreenContext = this.offscreenCanvas.getContext('2d');\n\n    this.repaint(true);\n  }\n\n  _getTimeScale(timeUnit: string) {\n    const scale: any = {};\n    for (const scaleValue of scaleValues) {\n      scale[scaleValue.unit] = scaleValue.factor;\n      if (scaleValue.unit === timeUnit) {\n        return scale;\n      }\n    }\n    return scale;\n  }\n\n  resetAssets() {\n    this.imageAssets = {};\n  }\n\n  _loadImage(imageUrl: string, assetName: string) {\n    const that = this;\n\n    const loadImage = (url: string, asset: keyof typeof that.imageAssets) => {\n      const image = new Image();\n      that.imageAssets[asset] = {\n        image,\n        loaded: false,\n      };\n\n      return new Promise((resolve, reject) => {\n        image.onload = () => resolve(asset);\n        image.onerror = () => reject(new Error(`load ${url} fail`));\n        image.src = url;\n      });\n    };\n    loadImage(imageUrl, assetName).then((asset: any) => {\n      that.imageAssets[asset].loaded = true;\n    });\n  }\n\n  _isImageLoaded(assetName: string) {\n    if (_.has(this.imageAssets, assetName) && this.imageAssets[assetName].loaded) {\n      return true;\n    } else {\n      return false;\n    }\n  }\n\n  _getImageAsset(assetName: string, resolveName = true) {\n    if (!_.has(this.imageAssets, assetName)) {\n      const { externalIcons } = this.controller.getSettings(true);\n      const assetUrl = assetUtils.getTypeSymbol(assetName, externalIcons, resolveName);\n      this._loadImage(assetUrl, assetName);\n    }\n\n    if (this._isImageLoaded(assetName)) {\n      return this.imageAssets[assetName].image;\n    } else {\n      return null;\n    }\n  }\n\n  _getAsset(assetName: string, relativeUrl: string) {\n    if (!_.has(this.imageAssets, assetName)) {\n      const assetUrl = assetUtils.getAssetUrl(relativeUrl);\n      this._loadImage(assetUrl, assetName);\n    }\n\n    if (this._isImageLoaded(assetName)) {\n      return this.imageAssets[assetName].image;\n    } else {\n      return null;\n    }\n  }\n\n  start() {\n    const that = this;\n    const repaintWrapper = () => {\n      that.repaint();\n      window.requestAnimationFrame(repaintWrapper);\n    };\n\n    window.requestAnimationFrame(repaintWrapper);\n\n    setInterval(() => {\n      that.fpsCounter = that.frameCounter;\n      that.frameCounter = 0;\n    }, 1000);\n  }\n\n  startAnimation() {\n    this.particleEngine.start();\n  }\n\n  stopAnimation() {\n    this.particleEngine.stop();\n    this.repaint();\n  }\n\n  _skipFrame() {\n    const now = Date.now();\n    const elapsedTime = now - this.lastRenderTime;\n\n    if (this.particleEngine.count() > 0) {\n      return false;\n    }\n\n    if (!this.controller.getSettings(true).animate && elapsedTime < 1000) {\n      return true;\n    }\n    return false;\n  }\n\n  repaint(forceRepaint = false) {\n    if (!forceRepaint && this._skipFrame()) {\n      return;\n    }\n    this.lastRenderTime = Date.now();\n\n    const ctx = this.context;\n    const cyCanvas = this.cyCanvas;\n    const offscreenCanvas = this.offscreenCanvas;\n    const offscreenContext = this.offscreenContext;\n    this.collisionDetector.reset();\n\n    offscreenCanvas.width = this.canvas.width;\n    offscreenCanvas.height = this.canvas.height;\n\n    // offscreen rendering\n    this._setTransformation(offscreenContext);\n\n    this.selectionNeighborhood = this.cytoscape.collection();\n    const selection = this.cytoscape.$(':selected');\n    selection.forEach((element: cytoscape.SingularElementArgument) => {\n      this.selectionNeighborhood.merge(element);\n\n      if (element.isNode()) {\n        const neighborhood = element.neighborhood();\n        this.selectionNeighborhood.merge(neighborhood);\n      } else {\n        const source = element.source();\n        const target = element.target();\n        this.selectionNeighborhood.merge(source);\n        this.selectionNeighborhood.merge(target);\n      }\n    });\n\n    this._drawEdgeAnimation(offscreenContext);\n    this._drawNodes(offscreenContext);\n\n    // static element rendering\n    // cyCanvas.resetTransform(ctx);\n    cyCanvas.clear(ctx);\n    if (this.controller.getSettings(true).showDebugInformation) {\n      this._drawDebugInformation();\n    }\n\n    if (offscreenCanvas.width > 0 && offscreenCanvas.height > 0) {\n      ctx.drawImage(offscreenCanvas, 0, 0);\n    }\n\n    // baseline animation\n    this.dashAnimationOffset = (Date.now() % 60000) / 250;\n  }\n\n  _setTransformation(ctx: CanvasRenderingContext2D) {\n    const pan = this.cytoscape.pan();\n    const zoom = this.cytoscape.zoom();\n    ctx.setTransform(1, 0, 0, 1, 0, 0);\n    ctx.translate(pan.x * this.pixelRatio, pan.y * this.pixelRatio);\n    ctx.scale(zoom * this.pixelRatio, zoom * this.pixelRatio);\n  }\n\n  _drawEdgeAnimation(ctx: CanvasRenderingContext2D) {\n    const now = Date.now();\n\n    ctx.save();\n\n    const edges = this.cytoscape.edges().toArray();\n    const hasSelection = this.selectionNeighborhood.size() > 0;\n\n    const transparentEdges = edges.filter((edge) => hasSelection && !this.selectionNeighborhood.has(edge));\n    const opaqueEdges = edges.filter((edge) => !hasSelection || this.selectionNeighborhood.has(edge));\n\n    ctx.globalAlpha = 0.25;\n    this._drawEdges(ctx, transparentEdges, now);\n    ctx.globalAlpha = 1;\n    this._drawEdges(ctx, opaqueEdges, now);\n    ctx.restore();\n  }\n\n  _drawEdges(ctx: CanvasRenderingContext2D, edges: cytoscape.EdgeSingular[], now: number) {\n    const cy = this.cytoscape;\n\n    for (const edge of edges) {\n      const sourcePoint = edge.sourceEndpoint();\n      const targetPoint = edge.targetEndpoint();\n      this._drawEdgeLine(ctx, edge, sourcePoint, targetPoint);\n      this._drawEdgeParticles(ctx, edge, sourcePoint, targetPoint, now);\n    }\n\n    const { showConnectionStats } = this.controller.getSettings(true);\n    if (showConnectionStats && cy.zoom() > 1) {\n      for (const edge of edges) {\n        this._drawEdgeLabel(ctx, edge);\n      }\n    }\n  }\n\n  _drawEdgeLine(\n    ctx: CanvasRenderingContext2D,\n    edge: cytoscape.EdgeSingular,\n    sourcePoint: cytoscape.Position,\n    targetPoint: cytoscape.Position\n  ) {\n    ctx.beginPath();\n\n    ctx.moveTo(sourcePoint.x, sourcePoint.y);\n    ctx.lineTo(targetPoint.x, targetPoint.y);\n\n    const metrics = edge.data('metrics');\n    const requestCount = _.get(metrics, 'normal', -1);\n    const errorCount = _.get(metrics, 'danger', -1);\n\n    let base;\n    if (!this.selectionNeighborhood.empty() && this.selectionNeighborhood.has(edge)) {\n      ctx.lineWidth = 3;\n      base = 140;\n    } else {\n      ctx.lineWidth = 1;\n      base = 80;\n    }\n\n    if (requestCount >= 0 && errorCount >= 0) {\n      const range = 255;\n\n      const factor = errorCount / requestCount;\n      const color = Math.min(255, base + range * Math.log2(factor + 1));\n\n      ctx.strokeStyle = 'rgb(' + color + ',' + base + ',' + base + ')';\n    } else {\n      ctx.strokeStyle = 'rgb(' + base + ',' + base + ',' + base + ')';\n    }\n\n    ctx.stroke();\n  }\n\n  _drawEdgeLabel(ctx: CanvasRenderingContext2D, edge: cytoscape.EdgeSingular) {\n    const { timeFormat } = this.controller.getSettings(true);\n\n    const midpoint = edge.midpoint();\n    const xMid = midpoint.x;\n    const yMid = midpoint.y;\n\n    let statistics: string[] = [];\n    const metrics: IntGraphMetrics = edge.data('metrics');\n    const duration = _.defaultTo(metrics.response_time, -1);\n    const requestCount = _.defaultTo(metrics.rate, -1);\n    const errorCount = _.defaultTo(metrics.error_rate, -1);\n\n    const timeScale = new humanFormat.Scale(this._getTimeScale(timeFormat));\n\n    if (duration >= 0) {\n      const decimals = duration >= 1000 ? 1 : 0;\n      statistics.push(humanFormat(duration, { scale: timeScale, decimals }));\n    }\n    if (requestCount >= 0) {\n      const decimals = requestCount >= 1000 ? 1 : 0;\n      statistics.push(humanFormat(parseFloat(requestCount.toString()), { decimals }) + ' Req.');\n    }\n    if (errorCount >= 0) {\n      const decimals = errorCount >= 1000 ? 1 : 0;\n      statistics.push(humanFormat(errorCount, { decimals }) + ' Err.');\n    }\n\n    if (statistics.length > 0) {\n      const edgeLabel = statistics.join(', ');\n      this._drawLabel(ctx, edgeLabel, xMid, yMid, edge);\n    }\n  }\n\n  _drawEdgeParticles(\n    ctx: CanvasRenderingContext2D,\n    edge: cytoscape.EdgeSingular,\n    sourcePoint: cytoscape.Position,\n    targetPoint: cytoscape.Position,\n    now: number\n  ) {\n    const particles: Particles = edge.data('particles');\n\n    if (particles === undefined) {\n      return;\n    }\n\n    const xVector = targetPoint.x - sourcePoint.x;\n    const yVector = targetPoint.y - sourcePoint.y;\n\n    const angle = Math.atan2(yVector, xVector);\n    const xDirection = Math.cos(angle);\n    const yDirection = Math.sin(angle);\n\n    const xMinLimit = Math.min(sourcePoint.x, targetPoint.x);\n    const xMaxLimit = Math.max(sourcePoint.x, targetPoint.x);\n    const yMinLimit = Math.min(sourcePoint.y, targetPoint.y);\n    const yMaxLimit = Math.max(sourcePoint.y, targetPoint.y);\n\n    const drawContext: DrawContext = {\n      ctx,\n      now,\n      xDirection,\n      yDirection,\n      xMinLimit,\n      xMaxLimit,\n      yMinLimit,\n      yMaxLimit,\n      sourcePoint,\n    };\n\n    // normal particles\n    ctx.beginPath();\n\n    let index = particles.normal.length - 1;\n    while (index >= 0) {\n      this._drawParticle(drawContext, particles.normal, index);\n      index--;\n    }\n\n    ctx.fillStyle = '#d1e2f2';\n    ctx.fill();\n\n    // danger particles\n    ctx.beginPath();\n\n    index = particles.danger.length - 1;\n    while (index >= 0) {\n      this._drawParticle(drawContext, particles.danger, index);\n      index--;\n    }\n\n    const dangerColor = this.controller.getSettings(true).style.dangerColor;\n    ctx.fillStyle = dangerColor;\n    ctx.fill();\n  }\n\n  _drawLabel(ctx: CanvasRenderingContext2D, label: string, cX: number, cY: number, edge: cytoscape.EdgeSingular) {\n    const labelPadding = 1;\n    ctx.font = '6px Arial';\n\n    const labelWidth = ctx.measureText(label).width;\n    let xPos = cX - labelWidth / 2;\n    let yPos = cY + 3;\n    let labelArea: Rectangle = {\n      coordinates: {\n        x: xPos - labelPadding + 4,\n        y: yPos - 6 - labelPadding + 4,\n      },\n      width: labelWidth,\n      height: 6,\n    };\n\n    if (!isNaN(labelArea.coordinates.x) || !isNaN(labelArea.coordinates.y) || !isNaN(xPos) || !isNaN(yPos)) {\n      // TODO: Rather than using a fixed number here, we should find a way to compute a second boundary condition smarter.\n      // This is for the case when all nodes are so close together the labels need to overlap each other.\n      const maxRepeats = 1000;\n      let repeats = 0;\n      while (this.collisionDetector.isColliding(labelArea) && repeats < maxRepeats) {\n        const nextPoint = this._getNextPointOnVector(xPos, yPos, edge, 0.999);\n        labelArea.coordinates = nextPoint;\n        yPos = nextPoint.y;\n        xPos = nextPoint.x;\n        repeats++;\n      }\n      this.collisionDetector.addRectangle(xPos - 4, yPos - 4, labelWidth + 12, 12);\n    }\n\n    ctx.fillStyle = this.colors.default;\n    ctx.fillRect(xPos - labelPadding, yPos - 6 - labelPadding, labelWidth + 2 * labelPadding, 6 + 2 * labelPadding);\n    ctx.fillStyle = this.colors.background;\n    ctx.fillText(label, xPos, yPos);\n  }\n\n  _getNextPointOnVector(x: number, y: number, edge: cytoscape.EdgeSingular, step: number) {\n    let yTarget = edge.sourceEndpoint().y;\n    let xTarget = edge.sourceEndpoint().x;\n\n    const newPoint: Point = {\n      x: xTarget * (1.0 - step) + x * step,\n      y: yTarget * (1.0 - step) + y * step,\n    };\n\n    return newPoint;\n  }\n\n  _drawParticle(drawCtx: DrawContext, particles: Particle[], index: number) {\n    const { ctx, now, xDirection, yDirection, xMinLimit, xMaxLimit, yMinLimit, yMaxLimit, sourcePoint } = drawCtx;\n\n    const particle = particles[index];\n\n    const timeDelta = now - particle.startTime;\n    const xPos = sourcePoint.x + xDirection * timeDelta * particle.velocity;\n    const yPos = sourcePoint.y + yDirection * timeDelta * particle.velocity;\n\n    if (xPos > xMaxLimit || xPos < xMinLimit || yPos > yMaxLimit || yPos < yMinLimit) {\n      // remove particle\n      particles.splice(index, 1);\n    } else {\n      // draw particle\n      ctx.moveTo(xPos, yPos);\n      ctx.arc(xPos, yPos, 1, 0, 2 * Math.PI, false);\n    }\n  }\n\n  _drawNodes(ctx: CanvasRenderingContext2D) {\n    const that = this;\n    const cy = this.cytoscape;\n\n    // Draw model elements\n    const nodes = cy.nodes().toArray();\n    for (let i = 0; i < nodes.length; i++) {\n      const node = nodes[i];\n      if (that.selectionNeighborhood.empty() || that.selectionNeighborhood.has(node)) {\n        ctx.globalAlpha = 1;\n      } else {\n        ctx.globalAlpha = 0.25;\n      }\n\n      // draw the node\n      if (node.data().type === 'PARENT') {\n        if (\n          node.data().layer >= this.controller.state.controller.state.currentLayer ||\n          node.data().layer === undefined\n        ) {\n          that._drawNode(ctx, node);\n        }\n      } else {\n        that._drawNode(ctx, node);\n      }\n\n      // drawing the node label in case we are not zoomed out\n      if (cy.zoom() > 1) {\n        that._drawNodeLabel(ctx, node);\n      }\n    }\n  }\n\n  _drawNode(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSingular) {\n    const cy = this.cytoscape;\n    const type = node.data('type');\n    const metrics: IntGraphMetrics = node.data('metrics');\n\n    if (type === EnGraphNodeType.INTERNAL) {\n      const requestCount = _.defaultTo(metrics.rate, -1);\n      const errorCount = _.defaultTo(metrics.error_rate, 0);\n      const responseTime = _.defaultTo(metrics.response_time, -1);\n      const threshold = _.defaultTo(metrics.threshold, -1);\n\n      let unknownPct;\n      let errorPct;\n      let healthyPct;\n      if (requestCount < 0) {\n        healthyPct = 0;\n        errorPct = 0;\n        unknownPct = 1;\n      } else {\n        if (errorCount <= 0) {\n          errorPct = 0.0;\n        } else {\n          errorPct = (1.0 / requestCount) * errorCount;\n        }\n        healthyPct = 1.0 - errorPct;\n        unknownPct = 0;\n      }\n\n      // drawing the donut\n      this._drawDonut(ctx, node, 15, 5, 0.5, [errorPct, unknownPct, healthyPct]);\n\n      // drawing the baseline status\n      const { showBaselines } = this.controller.getSettings(true);\n      if (showBaselines && responseTime >= 0 && threshold >= 0) {\n        const thresholdViolation = threshold < responseTime;\n\n        this._drawThresholdStroke(ctx, node, thresholdViolation, 15, 5, 0.5);\n      }\n      this._drawServiceIcon(ctx, node);\n    } else {\n      this._drawExternalService(ctx, node);\n    }\n\n    // draw statistics\n    if (cy.zoom() > 1) {\n      this._drawNodeStatistics(ctx, node);\n    }\n  }\n\n  _drawServiceIcon(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSingular) {\n    const nodeId: string = node.id();\n    const iconMappings = this.controller.getSettings(true).icons;\n\n    const mapping = _.find(iconMappings, ({ pattern }) => {\n      try {\n        return new RegExp(pattern).test(nodeId);\n      } catch (error) {\n        return false;\n      }\n    });\n\n    if (mapping) {\n      const image = this._getAsset(mapping.filename, mapping.filename + '.png');\n      if (image != null) {\n        const cX = node.position().x;\n        const cY = node.position().y;\n        const iconSize = 16;\n\n        ctx.drawImage(image, cX - iconSize / 2, cY - iconSize / 2, iconSize, iconSize);\n      }\n    }\n  }\n\n  _drawNodeStatistics(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSingular) {\n    const { timeFormat } = this.controller.getSettings(true);\n    const lines: string[] = [];\n\n    const metrics: IntGraphMetrics = node.data('metrics');\n    const requestCount = _.defaultTo(metrics.rate, -1);\n    const errorCount = _.defaultTo(metrics.error_rate, -1);\n    const responseTime = _.defaultTo(metrics.response_time, -1);\n\n    const timeScale = new humanFormat.Scale(this._getTimeScale(timeFormat));\n\n    if (requestCount >= 0) {\n      const decimals = requestCount >= 1000 ? 1 : 0;\n      lines.push('Requests: ' + humanFormat(parseFloat(requestCount.toString()), { decimals }));\n    }\n    if (errorCount >= 0) {\n      const decimals = errorCount >= 1000 ? 1 : 0;\n      lines.push('Errors: ' + humanFormat(errorCount, { decimals }));\n    }\n    if (responseTime >= 0) {\n      const decimals = responseTime >= 1000 ? 1 : 0;\n\n      lines.push('Avg. Resp. Time: ' + humanFormat(responseTime, { scale: timeScale, decimals }));\n    }\n\n    const pos = node.position();\n    const fontSize = 6;\n    const cX = pos.x + this.donutRadius * 1.25;\n    const cY = pos.y + fontSize / 2 - (fontSize / 2) * (lines.length - 1);\n\n    ctx.font = '6px Arial';\n    ctx.fillStyle = this.colors.default;\n    for (let i = 0; i < lines.length; i++) {\n      ctx.fillText(lines[i], cX, cY + i * fontSize);\n    }\n  }\n\n  _drawThresholdStroke(\n    ctx: CanvasRenderingContext2D,\n    node: cytoscape.NodeSingular,\n    violation: boolean,\n    radius: number,\n    width: number,\n    baseStrokeWidth: number\n  ) {\n    const pos = node.position();\n    const cX = pos.x;\n    const cY = pos.y;\n\n    const strokeWidth = baseStrokeWidth * 2 * (violation ? 1.5 : 1);\n    const offset = strokeWidth * 0.2;\n\n    ctx.beginPath();\n    ctx.arc(cX, cY, radius + strokeWidth - offset, 0, 2 * Math.PI, false);\n    ctx.closePath();\n    ctx.setLineDash([]);\n    ctx.lineWidth = strokeWidth * 1;\n    ctx.strokeStyle = 'white';\n    ctx.stroke();\n\n    ctx.beginPath();\n    ctx.arc(cX, cY, radius + strokeWidth - offset, 0, 2 * Math.PI, false);\n    ctx.closePath();\n\n    ctx.setLineDash([10, 2]);\n    if (violation && this.controller.getSettings(true).animate) {\n      ctx.lineDashOffset = this.dashAnimationOffset;\n    } else {\n      ctx.lineDashOffset = 0;\n    }\n    ctx.lineWidth = strokeWidth;\n    ctx.strokeStyle = violation ? 'rgb(184, 36, 36)' : '#37872d';\n\n    ctx.stroke();\n\n    // inner\n    ctx.beginPath();\n    ctx.arc(cX, cY, radius - width - baseStrokeWidth, 0, 2 * Math.PI, false);\n    ctx.closePath();\n    ctx.fillStyle = violation ? 'rgb(184, 36, 36)' : '#37872d';\n    ctx.fill();\n  }\n\n  _drawExternalService(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSingular) {\n    const pos = node.position();\n    const cX = pos.x;\n    const cY = pos.y;\n    const size = 12;\n\n    ctx.beginPath();\n    ctx.arc(cX, cY, 12, 0, 2 * Math.PI, false);\n    ctx.fillStyle = 'white';\n    ctx.fill();\n\n    ctx.beginPath();\n    ctx.arc(cX, cY, 11.5, 0, 2 * Math.PI, false);\n    ctx.fillStyle = this.colors.background;\n    ctx.fill();\n\n    const nodeType = node.data('external_type');\n\n    const image = this._getImageAsset(nodeType);\n    if (image != null) {\n      ctx.drawImage(image, cX - size / 2, cY - size / 2, size, size);\n    }\n  }\n\n  _drawNodeLabel(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSingular) {\n    const pos = node.position();\n    let label: string = node.id();\n    const labelPadding = 1;\n\n    if (this.selectionNeighborhood.empty() || !this.selectionNeighborhood.has(node)) {\n      if (label.length > 20) {\n        label = label.substr(0, 7) + '...' + label.slice(-7);\n      }\n    }\n\n    ctx.font = '6px Arial';\n\n    const labelWidth = ctx.measureText(label).width;\n    const xPos = pos.x - labelWidth / 2;\n    let yPos = pos.y + node.height() * 0.8;\n\n    if (node.data().type === 'PARENT') {\n      if (node.data().layer >= this.controller.state.controller.state.currentLayer || node.data().layer === undefined) {\n      } else {\n        yPos = pos.y + node.height() * 0.5 + 30;\n      }\n    }\n\n    const { showBaselines } = this.controller.getSettings(true);\n    const metrics: IntGraphMetrics = node.data('metrics');\n    const responseTime = _.defaultTo(metrics.response_time, -1);\n    const threshold = _.defaultTo(metrics.threshold, -1);\n\n    if (!showBaselines || threshold < 0 || responseTime < 0 || responseTime <= threshold) {\n      ctx.fillStyle = this.colors.default;\n    } else {\n      ctx.fillStyle = '#FF7383';\n    }\n\n    ctx.fillRect(xPos - labelPadding, yPos - 6 - labelPadding, labelWidth + 2 * labelPadding, 6 + 2 * labelPadding);\n\n    ctx.fillStyle = this.colors.background;\n    ctx.fillText(label, xPos, yPos);\n  }\n\n  _drawDebugInformation() {\n    const ctx = this.context;\n\n    this.frameCounter++;\n\n    ctx.font = '12px monospace';\n    ctx.fillStyle = 'white';\n    ctx.fillText('Frames per Second: ' + this.fpsCounter, 10, 12);\n    ctx.fillText('Particles: ' + this.particleEngine.count(), 10, 24);\n  }\n\n  _drawDonut(\n    ctx: CanvasRenderingContext2D,\n    node: cytoscape.NodeSingular,\n    radius: number,\n    width: number,\n    strokeWidth: number,\n    percentages: number[]\n  ) {\n    const cX = node.position().x;\n    const cY = node.position().y;\n    let currentArc = -Math.PI / 2; // offset\n\n    ctx.beginPath();\n    ctx.arc(cX, cY, radius + strokeWidth, 0, 2 * Math.PI, false);\n    ctx.closePath();\n    ctx.fillStyle = 'white';\n    ctx.fill();\n\n    const { healthyColor, dangerColor, noDataColor } = this.controller.getSettings(true).style;\n    const colors = [dangerColor, noDataColor, healthyColor];\n    for (let i = 0; i < percentages.length; i++) {\n      let arc = this._drawArc(ctx, currentArc, cX, cY, radius, percentages[i], colors[i]);\n      currentArc += arc;\n    }\n\n    ctx.beginPath();\n    ctx.arc(cX, cY, radius - width, 0, 2 * Math.PI, false);\n    ctx.fillStyle = 'white';\n    ctx.fill();\n\n    // cut out an inner-circle == donut\n    ctx.beginPath();\n    ctx.arc(cX, cY, radius - width - strokeWidth, 0, 2 * Math.PI, false);\n    if (node.selected()) {\n      ctx.fillStyle = 'white';\n    } else {\n      ctx.fillStyle = this.colors.background;\n    }\n    ctx.fill();\n  }\n\n  _drawArc(\n    ctx: CanvasRenderingContext2D,\n    currentArc: number,\n    cX: number,\n    cY: number,\n    radius: number,\n    percent: number,\n    color: string\n  ) {\n    // calc size of our wedge in radians\n    let WedgeInRadians = (percent * 360 * Math.PI) / 180;\n    // draw the wedge\n    ctx.save();\n    ctx.beginPath();\n    ctx.moveTo(cX, cY);\n    ctx.arc(cX, cY, radius, currentArc, currentArc + WedgeInRadians, false);\n    ctx.closePath();\n    ctx.fillStyle = color;\n    ctx.fill();\n    ctx.restore();\n    // sum the size of all wedges so far\n    // We will begin our next wedge at this sum\n    return WedgeInRadians;\n  }\n}\n"
  },
  {
    "path": "src/panel/canvas/particle_engine.ts",
    "content": "import CanvasDrawer from './graph_canvas';\nimport _ from 'lodash';\nimport { Particles, Particle, IntGraphMetrics } from '../../types';\n\nexport default class ParticleEngine {\n  drawer: CanvasDrawer;\n\n  maxVolume = 800;\n\n  minSpawnPropability = 0.004;\n\n  spawnInterval: NodeJS.Timeout;\n\n  animating: boolean;\n\n  constructor(canvasDrawer: CanvasDrawer) {\n    this.drawer = canvasDrawer;\n    this.animating = false;\n  }\n\n  start() {\n    this.animating = true;\n    if (!this.spawnInterval) {\n      const that = this;\n      this.spawnInterval = setInterval(() => that.animate(), 60);\n    }\n  }\n\n  stop() {\n    this.animating = false;\n  }\n\n  animate() {\n    const that = this;\n    if (!that.animating) {\n      if (!this.hasParticles()) {\n        clearInterval(this.spawnInterval);\n        this.spawnInterval = null;\n      }\n    } else {\n      that._spawnParticles();\n    }\n    that.drawer.repaint();\n  }\n\n  hasParticles() {\n    for (const edge of this.drawer.cytoscape.edges().toArray()) {\n      if (\n        edge.data('particles') !== undefined &&\n        (edge.data('particles').normal.length > 0 || edge.data('particles').danger.length > 0)\n      ) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  _spawnParticles() {\n    const cy = this.drawer.cytoscape;\n\n    const now = Date.now();\n    cy.edges().forEach((edge) => {\n      let particles: Particles = edge.data('particles');\n      const metrics: IntGraphMetrics = edge.data('metrics');\n\n      if (!metrics) {\n        return;\n      }\n\n      const rate = _.defaultTo(metrics.rate, 0);\n      const error_rate = _.defaultTo(metrics.error_rate, 0);\n      const volume = rate + error_rate;\n\n      let errorRate;\n      if (rate >= 0 && error_rate >= 0) {\n        errorRate = error_rate / rate;\n      } else {\n        errorRate = 0;\n      }\n\n      if (particles === undefined) {\n        particles = {\n          normal: [],\n          danger: [],\n        };\n        edge.data('particles', particles);\n      }\n\n      if (metrics && volume > 0) {\n        const spawnPropability = Math.min(volume / this.maxVolume, 1.0);\n        for (let i = 0; i < 5; i++) {\n          if (Math.random() <= spawnPropability + this.minSpawnPropability) {\n            const particle: Particle = {\n              velocity: 0.05 + Math.random() * 0.05,\n              startTime: now,\n            };\n            if (Math.random() < errorRate) {\n              particles.danger.push(particle);\n            } else {\n              particles.normal.push(particle);\n            }\n          }\n        }\n      }\n    });\n  }\n\n  count() {\n    const cy = this.drawer.cytoscape;\n\n    const count = _(cy.edges())\n      .map((edge) => edge.data('particles'))\n      .filter()\n      .map((particleArray) => particleArray.normal.length + particleArray.danger.length)\n      .sum();\n\n    return count;\n  }\n}\n"
  },
  {
    "path": "src/panel/layout_options.ts",
    "content": "const options = {\n  name: 'cola',\n  animate: true, // whether to show the layout as it's running\n  refresh: 1, // number of ticks per frame; higher is faster but more jerky\n  maxSimulationTime: 3000, // max length in ms to run the layout\n  ungrabifyWhileSimulating: false, // so you can't drag nodes during layout\n  fit: true, // set by controller // on every layout reposition of nodes, fit the viewport\n  padding: 90, // padding around the simulation\n  boundingBox: undefined as undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }\n  nodeDimensionsIncludeLabels: false, // whether labels should be included in determining the space used by a node\n\n  // layout event callbacks\n  ready: function () {}, // on layoutready\n  stop: function () {}, // on layoutstop\n\n  // positioning options\n  randomize: false, // use random node positions at beginning of layout\n  avoidOverlap: true, // if true, prevents overlap of node bounding boxes\n  handleDisconnected: true, // if true, avoids disconnected components from overlapping\n  convergenceThreshold: 0.01, // when the alpha value (system energy) falls below this value, the layout stops\n  nodeSpacing: function (node: any) {\n    return 50;\n  }, // extra spacing around nodes\n  flow: undefined as undefined, // use DAG/tree flow layout if specified, e.g. { axis: 'y', minSeparation: 30 }\n  alignment: undefined as undefined, // relative alignment constraints on nodes, e.g. function( node ){ return { x: 0, y: 1 } }\n  gapInequalities: undefined as undefined, // list of inequality constraints for the gap between the nodes, e.g. [{\"axis\":\"y\", \"left\":node1, \"right\":node2, \"gap\":25}]\n\n  // different methods of specifying edge length\n  // each can be a constant numerical value or a function like `function( edge ){ return 2; }`\n  edgeLength: undefined as undefined, // sets edge length directly in simulation\n  edgeSymDiffLength: undefined as undefined, // symmetric diff edge length in simulation\n  edgeJaccardLength: undefined as undefined, // jaccard edge length in simulation\n\n  // iterations of cola algorithm; uses default values on undefined\n  unconstrIter: 50, // set by controller // unconstrained initial layout iterations\n  userConstIter: undefined as undefined, // initial layout iterations with user-specified constraints\n  allConstIter: undefined as undefined, // initial layout iterations with all constraints including non-overlap\n\n  // infinite layout options\n  infinite: false, // overrides all other options for a forces-all-the-time mode\n};\n\nexport default options;\n"
  },
  {
    "path": "src/panel/serviceDependencyGraph/ServiceDependencyGraph.css",
    "content": ".service-dependency-graph-panel .graph-container {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    flex-direction: row;\n}\n\n.service-dependency-graph-panel .canvas-container {\n    width: 100%;\n    height: 100%;\n    overflow: hidden;\n}\n\n.service-dependency-graph-panel .zoom-button-container {\n    position: absolute;\n    top: 0;\n    right: 1rem;\n    z-index: 99;\n}\n"
  },
  {
    "path": "src/panel/serviceDependencyGraph/ServiceDependencyGraph.tsx",
    "content": "import CanvasDrawer from 'panel/canvas/graph_canvas';\nimport cytoscape, { EdgeCollection, EdgeSingular, ElementDefinition, NodeSingular } from 'cytoscape';\nimport React, { PureComponent } from 'react';\nimport { PanelController } from '../PanelController';\nimport cyCanvas from 'cytoscape-canvas';\nimport cola from 'cytoscape-cola';\nimport layoutOptions from '../layout_options';\nimport { Statistics } from '../statistics/Statistics';\nimport _ from 'lodash';\nimport {\n  TableContent,\n  IntGraphMetrics,\n  IntGraph,\n  IntGraphNode,\n  IntGraphEdge,\n  PanelSettings,\n  IntSelectionStatistics,\n} from 'types';\nimport { TemplateSrv, getTemplateSrv } from '@grafana/runtime';\nimport './ServiceDependencyGraph.css';\n\ninterface PanelState {\n  zoom: number | undefined;\n  animate: boolean | undefined;\n  controller: PanelController;\n  cy?: cytoscape.Core | undefined;\n  graphCanvas?: CanvasDrawer | undefined;\n  animateButtonClass?: string;\n  showStatistics: boolean;\n  data: IntGraph;\n  settings: PanelSettings;\n  layer: number | undefined;\n  maxLayer: number;\n  layerIncreaseFunction: any;\n  layerDecreaseFunction: any;\n}\n\ncyCanvas(cytoscape);\ncytoscape.use(cola);\n\nexport class ServiceDependencyGraph extends PureComponent<PanelState, PanelState> {\n  ref: any;\n\n  selectionId: string;\n\n  currentType: string;\n\n  selectionStatistics: IntSelectionStatistics;\n\n  receiving: TableContent[];\n\n  sending: TableContent[];\n\n  resolvedDrillDownLink: string;\n\n  templateSrv: TemplateSrv;\n\n  initResize = true;\n\n  constructor(props: PanelState) {\n    super(props);\n\n    let animateButtonClass = 'fa fa-play-circle';\n    if (props.animate) {\n      animateButtonClass = 'fa fa-pause-circle';\n    }\n\n    this.state = {\n      ...props,\n      showStatistics: false,\n      animateButtonClass: animateButtonClass,\n      animate: false,\n    };\n\n    this.ref = React.createRef();\n    this.templateSrv = getTemplateSrv();\n  }\n\n  componentDidMount() {\n    const cy: any = cytoscape({\n      container: this.ref,\n      zoom: this.state.zoom,\n      elements: this.props.data,\n      layout: {\n        name: 'cola',\n      },\n      style: [\n        {\n          selector: 'node',\n          css: {\n            'background-color': '#fbfbfb',\n            'background-opacity': 0,\n          },\n        },\n\n        {\n          selector: 'node:parent',\n          css: {\n            'background-opacity': 0.05,\n            shape: 'barrel',\n          },\n        },\n\n        {\n          selector: 'edge',\n          style: {\n            'curve-style': 'bezier',\n            'control-point-step-size': 100,\n            visibility: 'hidden',\n          },\n        },\n      ],\n      wheelSensitivity: 0.125,\n    });\n\n    let graphCanvas = new CanvasDrawer(\n      this,\n      cy,\n      cy.cyCanvas({\n        zIndex: 1,\n      })\n    );\n\n    cy.on('render cyCanvas.resize', () => {\n      graphCanvas.repaint(true);\n    });\n    cy.on('select', 'node', () => this.onSelectionChange());\n    cy.on('unselect', 'node', () => this.onSelectionChange());\n    this.setState({\n      cy: cy,\n      graphCanvas: graphCanvas,\n    });\n    graphCanvas.start();\n  }\n\n  componentDidUpdate() {\n    this._updateGraph(this.props.data);\n  }\n\n  _updateGraph(graph: IntGraph) {\n    const cyNodes = this._transformNodes(graph.nodes);\n    const cyEdges = this._transformEdges(graph.edges);\n\n    const nodes = this.state.cy.nodes().toArray();\n    const updatedNodes = this._updateOrRemove(nodes, cyNodes);\n\n    // add new nodes\n    this.state.cy.add(cyNodes);\n\n    const edges = this.state.cy.edges().toArray();\n    this._updateOrRemove(edges, cyEdges);\n\n    // add new edges\n    this.state.cy.add(cyEdges);\n\n    if (this.initResize) {\n      this.initResize = false;\n      this.state.cy.resize();\n      this.state.cy.reset();\n      this.runLayout();\n    } else {\n      if (cyNodes.length > 0) {\n        _.each(updatedNodes, (node) => {\n          node.lock();\n        });\n        this.runLayout(true);\n      }\n    }\n    this.state.graphCanvas.repaint(true);\n  }\n\n  _transformNodes(nodes: IntGraphNode[]): ElementDefinition[] {\n    const cyNodes: ElementDefinition[] = _.map(nodes, (node) => {\n      const result: ElementDefinition = {\n        group: 'nodes',\n        data: {\n          id: node.data.id,\n          type: node.data.type,\n          external_type: node.data.external_type,\n          parent: node.data.parent,\n          layer: node.data.layer,\n          metrics: {\n            ...node.data.metrics,\n          },\n        },\n      };\n      return result;\n    });\n\n    return cyNodes;\n  }\n\n  _transformEdges(edges: IntGraphEdge[]): ElementDefinition[] {\n    const cyEdges: ElementDefinition[] = _.map(edges, (edge) => {\n      const cyEdge: ElementDefinition = {\n        group: 'edges',\n        data: {\n          id: edge.data.source + ':' + edge.data.target,\n          source: edge.data.source,\n          target: edge.data.target,\n          metrics: {\n            ...edge.data.metrics,\n          },\n        },\n      };\n\n      return cyEdge;\n    });\n\n    return cyEdges;\n  }\n\n  _updateOrRemove(dataArray: Array<NodeSingular | EdgeSingular>, inputArray: ElementDefinition[]) {\n    const elements: any[] = []; //(NodeSingular | EdgeSingular)[]\n    for (let i = 0; i < dataArray.length; i++) {\n      const element = dataArray[i];\n\n      const cyNode = _.find(inputArray, { data: { id: element.id() } });\n\n      if (cyNode) {\n        element.data(cyNode.data);\n        _.remove(inputArray, (n) => n.data.id === cyNode.data.id);\n        elements.push(element);\n      } else {\n        element.remove();\n      }\n    }\n    return elements;\n  }\n\n  onSelectionChange() {\n    const selection = this.state.cy.$(':selected');\n\n    if (selection.length === 1) {\n      this.updateStatisticTable();\n      this.setState({\n        showStatistics: true,\n      });\n    } else {\n      this.setState({\n        showStatistics: false,\n      });\n    }\n  }\n\n  getSettings(resolveVariables: boolean): PanelSettings {\n    return this.state.controller.getSettings(resolveVariables);\n  }\n\n  toggleAnimation() {\n    let newValue = !this.state.animate;\n    let animateButtonClass = 'fa fa-play-circle';\n    if (newValue) {\n      this.state.graphCanvas.startAnimation();\n      animateButtonClass = 'fa fa-pause-circle';\n    } else {\n      this.state.graphCanvas.stopAnimation();\n    }\n    this.setState({\n      animate: newValue,\n      animateButtonClass: animateButtonClass,\n    });\n  }\n\n  runLayout(unlockNodes = false) {\n    const that = this;\n    const options = {\n      ...layoutOptions,\n\n      stop: function () {\n        if (unlockNodes) {\n          that.unlockNodes();\n        }\n        that.setState({\n          zoom: that.state.cy.zoom(),\n        });\n      },\n    };\n\n    this.state.cy.layout(options).run();\n  }\n\n  unlockNodes() {\n    this.state.cy.nodes().forEach((node: { unlock: () => void }) => {\n      node.unlock();\n    });\n  }\n\n  fit() {\n    const selection = this.state.graphCanvas.selectionNeighborhood;\n    if (selection && !selection.empty()) {\n      this.state.cy.fit(selection, 30);\n    } else {\n      this.state.cy.fit();\n    }\n    this.setState({\n      zoom: this.state.cy.zoom(),\n    });\n  }\n\n  zoom(zoom: number) {\n    const zoomStep = 0.25 * zoom;\n    const zoomLevel = Math.max(0.1, this.state.zoom + zoomStep);\n    this.setState({\n      zoom: zoomLevel,\n    });\n    this.state.cy.zoom(zoomLevel);\n    this.state.cy.center();\n  }\n\n  updateStatisticTable() {\n    const selection = this.state.cy.$(':selected');\n\n    if (selection.length === 1) {\n      const currentNode: NodeSingular = selection[0];\n      this.selectionId = currentNode.id().toString();\n      this.currentType = currentNode.data('type');\n      const receiving: TableContent[] = [];\n      const sending: TableContent[] = [];\n      const edges: EdgeCollection = selection.connectedEdges();\n\n      const metrics: IntGraphMetrics = selection.nodes()[0].data('metrics');\n\n      const requestCount = _.defaultTo(metrics.rate, -1);\n      const errorCount = _.defaultTo(metrics.error_rate, -1);\n      const duration = _.defaultTo(metrics.response_time, -1);\n      const threshold = _.defaultTo(metrics.threshold, -1);\n\n      this.selectionStatistics = {};\n\n      if (requestCount >= 0) {\n        this.selectionStatistics.requests = Math.floor(requestCount);\n      }\n      if (errorCount >= 0) {\n        this.selectionStatistics.errors = Math.floor(errorCount);\n      }\n      if (duration >= 0) {\n        this.selectionStatistics.responseTime = Math.floor(duration);\n\n        if (threshold >= 0) {\n          this.selectionStatistics.threshold = Math.floor(threshold);\n          this.selectionStatistics.thresholdViolation = duration > threshold;\n        }\n      }\n\n      for (let i = 0; i < edges.length; i++) {\n        const actualEdge: EdgeSingular = edges[i];\n        const sendingCheck: boolean = actualEdge.source().id() === this.selectionId;\n        let node: NodeSingular;\n\n        if (sendingCheck) {\n          node = actualEdge.target();\n        } else {\n          node = actualEdge.source();\n        }\n\n        const sendingObject: TableContent = {\n          name: node.id(),\n          responseTime: '-',\n          rate: '-',\n          error: '-',\n        };\n\n        const edgeMetrics: IntGraphMetrics = actualEdge.data('metrics');\n\n        if (edgeMetrics !== undefined) {\n          const { response_time, rate, error_rate } = edgeMetrics;\n\n          if (rate !== undefined) {\n            sendingObject.rate = Math.floor(rate).toString();\n          }\n          if (response_time !== undefined) {\n            sendingObject.responseTime = Math.floor(response_time) + ' ms';\n          }\n          if (error_rate !== undefined && rate !== undefined) {\n            sendingObject.error = Math.floor(error_rate / (rate / 100)) + '%';\n          }\n        }\n\n        if (sendingCheck) {\n          sending.push(sendingObject);\n        } else {\n          receiving.push(sendingObject);\n        }\n      }\n      this.receiving = receiving;\n      this.sending = sending;\n\n      this.generateDrillDownLink();\n    }\n  }\n\n  generateDrillDownLink() {\n    const { drillDownLink } = this.getSettings(false);\n    if (drillDownLink !== undefined) {\n      const link = drillDownLink.replace('{}', this.selectionId);\n      this.resolvedDrillDownLink = this.templateSrv.replace(link);\n    }\n  }\n\n  render() {\n    if (this.state.cy !== undefined) {\n      this._updateGraph(this.props.data);\n    }\n    return (\n      <div className=\"graph-container\">\n        <div className=\"service-dependency-graph\">\n          <div className=\"canvas-container\" ref={(ref) => (this.ref = ref)}></div>\n          <div className=\"zoom-button-container\">\n            <button className=\"btn navbar-button width-100\" onClick={() => this.toggleAnimation()}>\n              <i className={this.state.animateButtonClass}></i>\n            </button>\n            <button className=\"btn navbar-button width-100\" onClick={() => this.runLayout()}>\n              <i className=\"fa fa-sitemap\"></i>\n            </button>\n            <button className=\"btn navbar-button width-100\" onClick={() => this.fit()}>\n              <i className=\"fa fa-dot-circle-o\"></i>\n            </button>\n            <button className=\"btn navbar-button width-100\" onClick={() => this.props.layerIncreaseFunction()}>\n              <i className=\"fa fa-plus\"></i>\n            </button>\n            <button className=\"btn navbar-button width-100\" onClick={() => this.props.layerDecreaseFunction()}>\n              <i className=\"fa fa-minus\"></i>\n            </button>\n            <span>\n              Layer {this.state.controller.state.currentLayer}/{this.state.maxLayer}\n            </span>\n          </div>\n        </div>\n        <Statistics\n          show={this.state.showStatistics}\n          selectionId={this.selectionId}\n          resolvedDrillDownLink={this.resolvedDrillDownLink}\n          selectionStatistics={this.selectionStatistics}\n          currentType={this.currentType}\n          showBaselines={this.getSettings(true).showBaselines}\n          receiving={this.receiving}\n          sending={this.sending}\n        />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "src/panel/statistics/IncomingStatistics.tsx",
    "content": "import React from 'react';\nimport { TableContent } from 'types';\n\nexport const NodeStatistics = (receiving: TableContent[]) => {\n  return (\n    <>\n      <div className=\"secondHeader--selection\">Incoming Statistics</div>\n      {() => {\n        if (receiving.length > 0) {\n          return (\n            <table className=\"table--selection\">\n              <tr className=\"table--selection--head\">\n                <th>Name</th>\n                <th className=\"table--th--selectionSmall\">Time</th>\n                <th className=\"table--th--selectionSmall\">Requests</th>\n                <th className=\"table--th--selectionSmall\">Error Rate</th>\n              </tr>\n              {receiving.map((node: TableContent, index: number) => (\n                <tr key={'row-' + index}>\n                  <td className=\"table--td--selection\" title=\"{{node.name}}\">\n                    {node.name}\n                  </td>\n                  <td className=\"table--td--selection\">{node.responseTime}</td>\n                  <td className=\"table--td--selection\">{node.rate}</td>\n                  <td className=\"table--td--selection\">{node.error}</td>\n                </tr>\n              ))}\n            </table>\n          );\n        }\n        return <div className=\"no-data--selection\">No incoming statistics available.</div>;\n      }}\n    </>\n  );\n};\n"
  },
  {
    "path": "src/panel/statistics/NodeStatistics.tsx",
    "content": "import React from 'react';\nimport { IntTableHeader } from '../../types';\nimport { TableContent } from 'types';\nimport SortableTable from './SortableTable';\nimport roundPercentageToDecimal from './utils/Utils';\n\ninterface NodeStatisticsProps {\n  nodeList: TableContent[];\n  noDataText: string;\n  title: string;\n}\n\nconst tableHeaders: IntTableHeader[] = [\n  { text: 'Name', dataField: 'name', sort: true, isKey: true },\n  { text: 'Time', dataField: 'time', sort: true, ignoreLiteral: ' ms' },\n  { text: 'Requests', dataField: 'requests', sort: true, ignoreLiteral: '' },\n  { text: 'Error Rate', dataField: 'error_rate', sort: true, ignoreLiteral: '%' },\n];\n\nfunction getStatisticsTable(noDataText: string, nodeList: TableContent[]) {\n  if (nodeList.length > 0) {\n    return (\n      <SortableTable\n        tableHeaders={tableHeaders}\n        data={nodeList.map((node: TableContent) => {\n          return {\n            name: node.name,\n            time: node.responseTime,\n            requests: node.rate,\n            error_rate: roundPercentageToDecimal(2, node.error),\n          };\n        })}\n      />\n    );\n  } else {\n    return <div className=\"no-data--selection\">{noDataText}</div>;\n  }\n}\n\nexport const NodeStatistics: React.FC<NodeStatisticsProps> = ({ nodeList, noDataText, title }) => {\n  return (\n    <div>\n      <div className=\"secondHeader--selection\">{title}</div>\n      {getStatisticsTable(noDataText, nodeList)}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/panel/statistics/SortableTable.tsx",
    "content": "import React from 'react';\nimport { IntTableHeader, NodeData } from '../../types';\nimport BootstrapTable from 'react-bootstrap-table-next';\n\ninterface SortableTableProps {\n  tableHeaders: IntTableHeader[];\n  data: NodeData[];\n}\n\nfunction sort(a: string, b: string, order: string, ignoreLiteral: string) {\n  let cleanA = a.replace(ignoreLiteral, '');\n  let cleanB = b.replace(ignoreLiteral, '');\n  if ((order === 'asc' && cleanA === '-') || (order !== 'asc' && cleanB === '-')) {\n    return -1;\n  }\n  if ((order === 'asc' && cleanB === '-') || (order !== 'asc' && cleanA === '-')) {\n    return 1;\n  }\n  if (order === 'asc') {\n    return Number(cleanA) - Number(cleanB);\n  }\n  return Number(cleanB) - Number(cleanA);\n}\n\nexport const SortableTable: React.FC<SortableTableProps> = ({ tableHeaders, data }) => {\n  tableHeaders.forEach(function (value, i) {\n    value.classes = 'table--td--selection';\n    if (i !== 0) {\n      value.sortFunc = (a: string, b: string, order: string, _dataField: any, _rowA: any) => {\n        return sort(a, b, order, value.ignoreLiteral);\n      };\n    }\n  });\n\n  return (\n    <BootstrapTable\n      keyField=\"name\"\n      data={data}\n      columns={tableHeaders}\n      classes=\"table--selection\"\n      headerClasses=\"table--selection table--selection--head\"\n    />\n  );\n};\n\nexport default SortableTable;\n"
  },
  {
    "path": "src/panel/statistics/Statistics.css",
    "content": ".service-dependency-graph-panel .margin {\n    margin: 10px;\n}\n\n.service-dependency-graph-panel .statistics {\n    flex-basis: 0;\n    transition: flex-basis 500ms ease-in-out;\n    overflow-y: scroll;\n}\n\n.service-dependency-graph-panel .statistics.show {\n  flex-basis: 30rem;\n  padding-left: 0.5%;\n}\n"
  },
  {
    "path": "src/panel/statistics/Statistics.tsx",
    "content": "import React from 'react';\nimport { NodeStatistics } from './NodeStatistics';\nimport '../../css/novatec-service-dependency-graph-panel.css';\nimport './Statistics.css';\nimport { IntSelectionStatistics, TableContent } from 'types';\nimport roundPercentageToDecimal from './utils/Utils';\n\ninterface StatisticsProps {\n  show: boolean;\n  selectionId: string | number;\n  resolvedDrillDownLink: string;\n  selectionStatistics: IntSelectionStatistics;\n  currentType: string;\n  showBaselines: boolean;\n  receiving: TableContent[];\n  sending: TableContent[];\n}\n\nexport const Statistics: React.FC<StatisticsProps> = ({\n  show,\n  selectionId,\n  resolvedDrillDownLink,\n  selectionStatistics,\n  currentType,\n  showBaselines,\n  receiving,\n  sending,\n}) => {\n  let statisticsClass = 'statistics';\n  let statistics = <div></div>;\n  if (show) {\n    statisticsClass = 'statistics show ';\n    let drilldownLink = <div></div>;\n    if (resolvedDrillDownLink && resolvedDrillDownLink.length > 0 && currentType === 'INTERNAL') {\n      drilldownLink = (\n        <a target=\"_blank\" rel=\"noreferrer\" href={resolvedDrillDownLink}>\n          <i className=\"fa fa-paper-plane-o margin\"></i>\n        </a>\n      );\n    }\n\n    const requests =\n      selectionStatistics.requests >= 0 ? (\n        <tr>\n          <td className=\"table--td--selection\">Requests</td>\n          <td className=\"table--td--selection\">{selectionStatistics.requests}</td>\n        </tr>\n      ) : null;\n\n    const errors =\n      selectionStatistics.errors >= 0 ? (\n        <tr>\n          <td className=\"table--td--selection\">Errors</td>\n          <td className=\"table--td--selection\">{selectionStatistics.errors}</td>\n        </tr>\n      ) : null;\n\n    let errorRate =\n      selectionStatistics.requests > 0 && selectionStatistics.errors >= 0 ? (\n        <tr>\n          <td className=\"table--td--selection\">Error Rate</td>\n          <td className=\"table--td--selection\">\n            {roundPercentageToDecimal(\n              2,\n              ((100 / selectionStatistics.requests) * selectionStatistics.errors).toString()\n            )}\n          </td>\n        </tr>\n      ) : null;\n\n    let avgResponseTime =\n      selectionStatistics.responseTime >= 0 ? (\n        <tr>\n          <td className=\"table--td--selection\">Avg. Response Time</td>\n          <td className=\"table--td--selection\">{selectionStatistics.responseTime} ms</td>\n        </tr>\n      ) : null;\n    let threshold = selectionStatistics.thresholdViolation ? (\n      <td className=\"table--td--selection threshold--bad\">\n        Bad ({'>'} {selectionStatistics.threshold}ms)\n      </td>\n    ) : (\n      <td className=\"table--td--selection threshold--good\"> Good (&lt;= {selectionStatistics.threshold}ms) </td>\n    );\n    let baseline =\n      showBaselines && selectionStatistics.threshold ? (\n        <tr>\n          <td className=\"table--td--selection\">Response Time Health (Upper Baseline)</td>\n          {threshold}\n        </tr>\n      ) : null;\n\n    statistics = (\n      <div className=\"statistics\">\n        <div className=\"header--selection\">\n          {selectionId}\n          {drilldownLink}\n        </div>\n\n        <div className=\"secondHeader--selection\">Statistics</div>\n        <table className=\"table--selection\">\n          <tr className=\"table--selection--head\">\n            <th>Name</th>\n            <th className=\"table--th--selectionMedium\">Value</th>\n          </tr>\n          {requests}\n          {errors}\n          {errorRate}\n          {avgResponseTime}\n          {baseline}\n        </table>\n\n        <NodeStatistics\n          nodeList={receiving}\n          noDataText=\"No incoming statistics available.\"\n          title=\"Incoming Statistics\"\n        />\n        <NodeStatistics nodeList={sending} noDataText=\"No outgoing statistics available.\" title=\"Outgoing Statistics\" />\n      </div>\n    );\n  }\n  return <div className={statisticsClass}>{statistics}</div>;\n};\n"
  },
  {
    "path": "src/panel/statistics/utils/Utils.ts",
    "content": "function roundPercentageToDecimal(decimal: number, value: string) {\n  if (value !== '-') {\n    let valueDecimals = _getDecimalsOf(parseFloat(value));\n    if (valueDecimals > decimal) {\n      value = parseFloat(value).toFixed(decimal) + '%';\n    }\n  }\n  return value;\n}\n\nfunction _getDecimalsOf(value: number) {\n  if (Math.floor(value) !== value) {\n    return value.toString().split('.')[1].length || 0;\n  }\n  return 0;\n}\n\nexport default roundPercentageToDecimal;\n"
  },
  {
    "path": "src/plugin.json",
    "content": "{\n  \"type\": \"panel\",\n  \"name\": \"Service Dependency Graph\",\n  \"id\": \"novatec-sdg-panel\",\n  \"info\": {\n    \"version\": \"4.2.0\",\n    \"updated\": \"2025-03-26\",\n    \"description\": \"Service Dependency Graph panel for Grafana. Shows metric-based, dynamic dependency graph between services, indicates responsetime, load and error rate statistic for individual services and communication edges. Shows communication to external services, such as Web calls, database calls, message queues, LDAP calls, etc. Provides a details dialog for each selected service that shows statistics about incoming and outgoing traffic.\",\n    \"author\": {\n      \"name\": \"Novatec Consulting GmbH\",\n      \"url\": \"https://www.novatec-gmbh.de/\"\n    },\n    \"keywords\": [\n      \"grafana\",\n      \"plugin\",\n      \"service-dependency-graph\",\n      \"topology\"\n    ],\n    \"logos\": {\n      \"small\": \"img/novatec_sdg_panel_logo.svg\",\n      \"large\": \"img/novatec_sdg_panel_logo.svg\"\n    },\n    \"links\": [\n      {\n        \"name\": \"Project site\",\n        \"url\": \"https://github.com/NovatecConsulting/novatec-service-dependency-graph-panel.git\"\n      },\n      {\n        \"name\": \"Apache License\",\n        \"url\": \"https://github.com/NovatecConsulting/novatec-service-dependency-graph-panel/blob/master/LICENSE\"\n      }\n    ],\n    \"screenshots\": [\n      {\n        \"name\": \"Showcase\",\n        \"path\": \"img/data-example-1.png\"\n      }\n    ]\n  },\n  \"dependencies\": {\n    \"grafanaDependency\": \">=10.4.0\",\n    \"plugins\": []\n  }\n}\n"
  },
  {
    "path": "src/processing/graph_generator.ts",
    "content": "import _ from 'lodash';\nimport { isPresent } from './utils/Utils';\nimport { PanelController } from '../panel/PanelController';\nimport {\n  GraphDataElement,\n  IntGraph,\n  IntGraphEdge,\n  IntGraphMetrics,\n  IntGraphNode,\n  EnGraphNodeType,\n  GraphDataType,\n} from '../types';\nimport NodeTree from './node_tree';\nimport NodeSubstitutor from './node_substitutor';\n\nclass GraphGenerator {\n  controller: PanelController;\n  nodeSubstitutor: NodeSubstitutor;\n\n  constructor(controller: PanelController) {\n    this.controller = controller;\n    this.nodeSubstitutor = new NodeSubstitutor();\n  }\n\n  _createNode(dataElements: GraphDataElement[], nodeTree: NodeTree): IntGraphNode | undefined {\n    if (!dataElements || dataElements.length <= 0) {\n      return undefined;\n    }\n\n    const sumMetrics = this.controller.getSettings(true).sumTimings;\n\n    let nodeName = dataElements[0].target;\n    if (nodeName === '' || nodeName === undefined || nodeName === null) {\n      nodeName = 'undefined';\n    }\n\n    const internalNode =\n      _.some(dataElements, ['type', GraphDataType.INTERNAL]) ||\n      _.some(dataElements, ['type', GraphDataType.EXTERNAL_IN]);\n    const nodeType = internalNode ? EnGraphNodeType.INTERNAL : EnGraphNodeType.EXTERNAL;\n\n    const metrics: IntGraphMetrics = {};\n\n    const node: IntGraphNode = {\n      data: {\n        id: nodeName,\n        label: nodeName,\n        external_type: nodeType,\n        type: nodeType,\n        layer: 0,\n        metrics,\n        namespace: [],\n      },\n    };\n\n    //get first element where namespace is defined.\n    const namespaceElement = dataElements.find((el) => el.namespace !== undefined);\n    if (namespaceElement) {\n      const namespace = namespaceElement.namespace;\n      node.data.namespace = namespace;\n      node.data.layer = namespace.length;\n      node.data.parent = namespace[namespace.length - 1];\n      this._updateMaxLayer(node.data.layer);\n    }\n\n    const aggregationFunction = sumMetrics ? _.sum : _.mean;\n\n    if (internalNode) {\n      metrics.rate = _.sum(_.map(dataElements, (element) => element.data.rate_in));\n      metrics.error_rate = _.sum(_.map(dataElements, (element) => element.data.error_rate_in));\n\n      const response_timings = _.map(dataElements, (element) => element.data.response_time_in).filter(isPresent);\n      if (response_timings.length > 0) {\n        metrics.response_time = aggregationFunction(response_timings);\n      }\n    } else {\n      metrics.rate = _.sum(_.map(dataElements, (element) => element.data.rate_out));\n      metrics.error_rate = _.sum(_.map(dataElements, (element) => element.data.error_rate_out));\n\n      const response_timings = _.map(dataElements, (element) => element.data.response_time_out).filter(isPresent);\n      if (response_timings.length > 0) {\n        metrics.response_time = aggregationFunction(response_timings);\n      }\n\n      const externalType = _(dataElements)\n        .map((element) => element.data.type)\n        .uniq()\n        .value();\n\n      if (externalType.length === 1) {\n        node.data.external_type = externalType[0];\n      }\n    }\n\n    // metrics which are same for internal and external nodes\n    metrics.threshold = _(dataElements)\n      .map((element) => element.data.threshold)\n      .filter()\n      .mean();\n\n    if (sumMetrics) {\n      const requestCount = _.defaultTo(metrics.rate, 0) + _.defaultTo(metrics.error_rate, 0);\n      const response_time = _.defaultTo(metrics.response_time, -1);\n      if (requestCount > 0 && response_time >= 0) {\n        metrics.response_time = response_time / requestCount;\n      }\n    }\n\n    const { rate, error_rate } = metrics;\n    if (rate + error_rate > 0) {\n      metrics.success_rate = (1.0 / (rate + error_rate)) * rate;\n    } else {\n      metrics.success_rate = 1.0;\n    }\n\n    nodeTree.addNode(node);\n    this.nodeSubstitutor.add(node);\n    return node;\n  }\n\n  _createMissingNodes(data: GraphDataElement[], nodes: IntGraphNode[]): IntGraphNode[] {\n    const existingNodeNames = _.map(nodes, (node) => node.data.id);\n    const expectedNodeNames = _.uniq(_.flatMap(data, (dataElement) => [dataElement.source, dataElement.target])).filter(\n      isPresent\n    );\n    const missingNodeNames = _.difference(expectedNodeNames, existingNodeNames);\n    const missingNodes = _.map(missingNodeNames, (name) => {\n      let nodeType: EnGraphNodeType;\n      let external_type: string | undefined;\n\n      // derive node type\n      let elementSrc = _.find(data, { source: name });\n      let elementTrgt = _.find(data, { target: name });\n      if (elementSrc && elementSrc.type === GraphDataType.EXTERNAL_IN) {\n        nodeType = EnGraphNodeType.EXTERNAL;\n        external_type = elementSrc.data.type;\n      } else if (elementTrgt && elementTrgt.type === GraphDataType.EXTERNAL_OUT) {\n        nodeType = EnGraphNodeType.EXTERNAL;\n        external_type = elementTrgt.data.type;\n      } else {\n        nodeType = EnGraphNodeType.INTERNAL;\n      }\n      let value: IntGraphNode = {\n        data: {\n          id: name,\n          type: nodeType,\n          external_type: external_type,\n          metrics: {},\n          layer: 0,\n        },\n      };\n      this.nodeSubstitutor.add(value);\n      return value;\n    });\n    return missingNodes;\n  }\n\n  _createNodes(data: GraphDataElement[]): IntGraphNode[] {\n    let tree = new NodeTree();\n    const filteredData = _.filter(\n      data,\n      (dataElement) =>\n        dataElement.source !== dataElement.target ||\n        (_.has(dataElement, 'target') && !_.has(dataElement, 'target')) ||\n        (!_.has(dataElement, 'target') && _.has(dataElement, 'target'))\n    );\n\n    const targetGroups = _.groupBy(filteredData, 'target');\n\n    const explicitlyNamedNodes = _.map(targetGroups, (group) => this._createNode(group, tree)).filter(isPresent);\n\n    // ensure that all nodes exist, even we have no data for them\n    const missingNodes = this._createMissingNodes(filteredData, explicitlyNamedNodes);\n    missingNodes.forEach((node) => tree.addNode(node));\n    const allNodes = tree.getNodesFromLayer(this.controller.state.currentLayer);\n    return allNodes;\n  }\n\n  _resolveSubstitute(name: string): string {\n    return this.nodeSubstitutor.substituteUntilLayer(\n      name,\n      this.controller.state.currentLayer,\n      this.controller.maxLayer\n    );\n  }\n\n  _createEdge(dataElement: GraphDataElement): IntGraphEdge | undefined {\n    let { source, target } = dataElement;\n    if (source === undefined || target === undefined) {\n      console.error('source and target are necessary to create an edge', dataElement);\n      return undefined;\n    }\n\n    const metrics: IntGraphMetrics = {};\n\n    source = this._resolveSubstitute(source);\n    target = this._resolveSubstitute(target);\n    if (source === target) {\n      return undefined;\n    }\n\n    const edge: IntGraphEdge = {\n      source: source,\n      target: target,\n      data: {\n        source,\n        target,\n        metrics,\n      },\n    };\n\n    const { rate_out, rate_in, error_rate_out, response_time_out } = dataElement.data;\n    if (!_.isUndefined(rate_out)) {\n      metrics.rate = rate_out;\n    } else if (!_.isUndefined(rate_in)) {\n      metrics.rate = rate_in;\n    }\n    if (!_.isUndefined(error_rate_out)) {\n      metrics.error_rate = error_rate_out;\n    }\n    if (!_.isUndefined(response_time_out)) {\n      const { sumTimings } = this.controller.getSettings(true);\n\n      if (sumTimings && metrics.rate) {\n        metrics.response_time = response_time_out / metrics.rate;\n      } else {\n        metrics.response_time = response_time_out;\n      }\n    }\n\n    return edge;\n  }\n\n  _resolveEdgeMap(edges: IntGraphEdge[]) {\n    let edgeMap: Map<string, IntGraphEdge[]> = new Map();\n    edges.forEach((edge) => {\n      if (edgeMap.get(edge.source + '-' + edge.target)) {\n        edgeMap.get(edge.source + '-' + edge.target).push(edge);\n      } else {\n        edgeMap.set(edge.source + '-' + edge.target, [edge]);\n      }\n    });\n    return edgeMap;\n  }\n\n  _mergeArrayOfEdges(edges: IntGraphEdge[]) {\n    let errorRateCounter = 0;\n    let rateCounter = 0;\n    let responseTimeCounter = 0;\n    let successRateCounter = 0;\n    let thresholdCounter = 0;\n\n    const mergedEdge: IntGraphEdge = {\n      target: '',\n      source: '',\n      data: {\n        source: '',\n        target: '',\n        metrics: {},\n      },\n    };\n    edges.forEach((edge) => {\n      if (mergedEdge.source === '') {\n        mergedEdge.source = edge.source;\n        mergedEdge.data.source = edge.data.source;\n      }\n      if (mergedEdge.target === '') {\n        mergedEdge.target = edge.target;\n        mergedEdge.data.target = edge.data.target;\n      }\n      if (edge.data.metrics.error_rate) {\n        mergedEdge.data.metrics.error_rate = mergedEdge.data.metrics.error_rate\n          ? mergedEdge.data.metrics.error_rate + edge.data.metrics.error_rate\n          : (mergedEdge.data.metrics.error_rate = edge.data.metrics.error_rate);\n        errorRateCounter++;\n      }\n      if (edge.data.metrics.rate) {\n        mergedEdge.data.metrics.rate = mergedEdge.data.metrics.rate\n          ? mergedEdge.data.metrics.rate + edge.data.metrics.rate\n          : (mergedEdge.data.metrics.rate = edge.data.metrics.rate);\n        rateCounter++;\n      }\n      if (edge.data.metrics.response_time) {\n        mergedEdge.data.metrics.response_time = mergedEdge.data.metrics.response_time\n          ? mergedEdge.data.metrics.response_time + edge.data.metrics.response_time\n          : (mergedEdge.data.metrics.response_time = edge.data.metrics.response_time);\n        responseTimeCounter++;\n      }\n      if (edge.data.metrics.success_rate) {\n        mergedEdge.data.metrics.success_rate = mergedEdge.data.metrics.success_rate\n          ? mergedEdge.data.metrics.success_rate + edge.data.metrics.success_rate\n          : (mergedEdge.data.metrics.success_rate = edge.data.metrics.success_rate);\n        successRateCounter++;\n      }\n      if (edge.data.metrics.threshold) {\n        mergedEdge.data.metrics.threshold = mergedEdge.data.metrics.threshold\n          ? mergedEdge.data.metrics.threshold + edge.data.metrics.threshold\n          : (mergedEdge.data.metrics.threshold = edge.data.metrics.threshold);\n        thresholdCounter++;\n      }\n    });\n\n    if (mergedEdge.data.metrics.error_rate) {\n      mergedEdge.data.metrics.error_rate = mergedEdge.data.metrics.error_rate / errorRateCounter;\n    }\n    if (mergedEdge.data.metrics.rate) {\n      mergedEdge.data.metrics.rate = mergedEdge.data.metrics.rate / rateCounter;\n    }\n    if (mergedEdge.data.metrics.response_time) {\n      mergedEdge.data.metrics.response_time = mergedEdge.data.metrics.response_time / responseTimeCounter;\n    }\n    if (mergedEdge.data.metrics.success_rate) {\n      mergedEdge.data.metrics.success_rate = mergedEdge.data.metrics.success_rate / successRateCounter;\n    }\n    if (mergedEdge.data.metrics.threshold) {\n      mergedEdge.data.metrics.threshold = mergedEdge.data.metrics.threshold / thresholdCounter;\n    }\n\n    return mergedEdge;\n  }\n\n  _edgeMapToMergedEdges(edgeMap: Map<string, IntGraphEdge[]>) {\n    let edges: IntGraphEdge[] = [];\n    for (const entry of edgeMap.values()) {\n      edges.push(this._mergeArrayOfEdges(entry));\n    }\n    return edges;\n  }\n\n  _mergeEdges(edges: IntGraphEdge[]) {\n    const edgeMap = this._resolveEdgeMap(edges);\n    this._edgeMapToMergedEdges(edgeMap);\n\n    return edges;\n  }\n\n  _createEdges(data: GraphDataElement[]): IntGraphEdge[] {\n    const filteredData = _(data)\n      .filter((e) => !!e.source)\n      .filter((e) => e.source !== e.target)\n      .filter((e) => e.target !== null || e.source !== null)\n      .value();\n\n    const edges = _.map(filteredData, (element) => this._createEdge(element));\n    const filteredEdges = edges.filter(isPresent);\n    return this._mergeEdges(filteredEdges);\n  }\n\n  _filterData(graph: IntGraph): IntGraph {\n    const { filterEmptyConnections: filterData } = this.controller.getSettings(true);\n\n    if (filterData) {\n      const filteredGraph: IntGraph = {\n        nodes: [],\n        edges: [],\n      };\n\n      // filter empty connections\n      filteredGraph.edges = _.filter(graph.edges, (edge) => _.size(edge.data.metrics) > 0);\n\n      filteredGraph.nodes = _.filter(graph.nodes, (node) => {\n        const id = node.data.id;\n\n        // don't filter connected elements and parents\n        if (\n          _.some(graph.edges, { source: id }) ||\n          _.some(graph.edges, { target: id }) ||\n          node.data.type === EnGraphNodeType.PARENT\n        ) {\n          return true;\n        }\n\n        const metrics = node.data.metrics;\n        if (!metrics) {\n          return false; // no metrics\n        }\n\n        // only if rate, error rate or response time is available\n        return (\n          _.defaultTo(metrics.rate, -1) >= 0 ||\n          _.defaultTo(metrics.error_rate, -1) >= 0 ||\n          _.defaultTo(metrics.response_time, -1) >= 0\n        );\n      });\n\n      return filteredGraph;\n    } else {\n      return graph;\n    }\n  }\n\n  generateGraph(graphData: GraphDataElement[]): IntGraph {\n    const nodes = this._createNodes(graphData);\n\n    const edges = this._createEdges(graphData);\n    const graph: IntGraph = {\n      nodes,\n      edges,\n    };\n\n    const filteredGraph = this._filterData(graph);\n    return filteredGraph;\n  }\n\n  _updateMaxLayer(layer: number) {\n    if (layer > this.controller.maxLayer) {\n      this.controller.maxLayer = layer;\n    }\n  }\n}\n\nexport default GraphGenerator;\n"
  },
  {
    "path": "src/processing/node_substitutor.ts",
    "content": "import _ from 'lodash';\nimport { IntGraphNode } from '../types';\n\nclass NodeSubstitutor {\n  private _substitutionMap: any;\n\n  constructor() {\n    this._substitutionMap = new Map();\n  }\n\n  add(node: IntGraphNode) {\n    const nameSpace = node.data.namespace;\n    if (nameSpace && nameSpace.length > 0) {\n      let currentValue = nameSpace;\n      let currentKey = node.data.label;\n      this._substitutionMap.set(currentKey, currentValue);\n    }\n  }\n\n  substituteUntilLayer(nodeName: string, layer: number, maxLayer: number) {\n    if (!this._substitutionMap.has(nodeName)) {\n      return nodeName;\n    }\n    const nameSpace = this._substitutionMap.get(nodeName);\n    const nsLeng = nameSpace.length;\n    if (nsLeng - 1 < layer) {\n      return nodeName;\n    }\n\n    return nameSpace[layer];\n  }\n}\n\nexport default NodeSubstitutor;\n"
  },
  {
    "path": "src/processing/node_tree.ts",
    "content": "import _ from 'lodash';\nimport { EnGraphNodeType, IntGraphMetrics, IntGraphNode, NodeTreeElement } from '../types';\n\nclass NodeTree {\n  private _root: NodeTreeElement;\n  private _metricMap: any;\n\n  constructor() {\n    this._root = { id: 'root', children: [] };\n    this._metricMap = {};\n  }\n\n  addNode(node: IntGraphNode) {\n    if (node.data.id !== 'undefined') {\n      this._addNode({ id: node.data.id, node: node, children: [] }, this._root, 0);\n    }\n  }\n\n  getNodesFromLayer(layer: number) {\n    let nodes = this._getNodesFromLayer(this._root, layer, 0);\n    nodes.forEach((element) => {\n      if (this._metricMap[element.data.id]) {\n        (Object.keys(element.data.metrics) as Array<keyof typeof element.data.metrics>).forEach(\n          (key) => (element.data.metrics[key] = element.data.metrics[key] / this._metricMap[element.data.id][key])\n        );\n      }\n    });\n    return this._getNodesFromLayer(this._root, layer, 0);\n  }\n\n  getNamePath(namePath: string[]) {\n    let currentLayer = this._root;\n    namePath.forEach((element) => {\n      currentLayer = this._getObjectFromArray(currentLayer.children, element);\n    });\n    return currentLayer;\n  }\n\n  private _getNodesFromLayer(currentNode: NodeTreeElement, layer: number, layerCounter: number): IntGraphNode[] {\n    let children;\n    if (layer === layerCounter) {\n      children = currentNode.children.map((element) => element.node);\n      if (currentNode !== this._root) {\n        children.push(currentNode.node);\n      }\n      return children;\n    }\n    layerCounter++;\n    children = _.flatten(currentNode.children.map((element) => this._getNodesFromLayer(element, layer, layerCounter)));\n    if (currentNode !== this._root) {\n      children.push(currentNode.node);\n    }\n    return children;\n  }\n\n  private _getNameSpaceFromCurrentLevel(namespace: string[], currentLevel: number) {\n    let nameSpaces = [];\n    for (let i = 0; i < currentLevel; i++) {\n      nameSpaces.push(namespace[i]);\n    }\n    return nameSpaces;\n  }\n\n  private _sumMetrics(sourceNode: IntGraphNode, targetNode: IntGraphNode): IntGraphMetrics {\n    const source = sourceNode.data.metrics;\n    const target = targetNode.data.metrics;\n    let metrics: IntGraphMetrics = {};\n    if (!this._metricMap[targetNode.data.id]) {\n      this._metricMap[targetNode.data.id] = {};\n    }\n    if (target.rate || source.rate) {\n      metrics.rate = (target.rate ? target.rate : 0) + (source.rate ? source.rate : 0);\n      if (target.rate && source.rate && !isNaN(target.rate) && !isNaN(source.rate)) {\n        this._metricMap[targetNode.data.id].rate\n          ? (this._metricMap[targetNode.data.id].rate = this._metricMap[targetNode.data.id].rate + 1)\n          : (this._metricMap[targetNode.data.id].rate = 1);\n      } else {\n        if (!this._metricMap[targetNode.data.id].rate) {\n          this._metricMap[targetNode.data.id].rate = 1;\n        }\n      }\n    }\n\n    if (target.response_time || source.response_time) {\n      metrics.response_time =\n        (target.response_time ? target.response_time : 0) + (source.response_time ? source.response_time : 0);\n      if (\n        target.response_time &&\n        source.response_time &&\n        !isNaN(target.response_time) &&\n        !isNaN(source.response_time)\n      ) {\n        this._metricMap[targetNode.data.id].response_time\n          ? (this._metricMap[targetNode.data.id].response_time = this._metricMap[targetNode.data.id].response_time + 1)\n          : (this._metricMap[targetNode.data.id].response_time = 1);\n      } else {\n        if (!this._metricMap[targetNode.data.id].response_time) {\n          this._metricMap[targetNode.data.id].response_time = 1;\n        }\n      }\n    }\n\n    if (target.success_rate || source.success_rate) {\n      metrics.success_rate =\n        (target.success_rate ? target.success_rate : 0) + (source.success_rate ? source.success_rate : 0);\n      if (target.success_rate && source.success_rate && !isNaN(target.success_rate) && !isNaN(source.success_rate)) {\n        this._metricMap[targetNode.data.id].success_rate\n          ? (this._metricMap[targetNode.data.id].success_rate = this._metricMap[targetNode.data.id].success_rate + 1)\n          : (this._metricMap[targetNode.data.id].success_rate = 1);\n      } else {\n        if (!this._metricMap[targetNode.data.id].success_rate) {\n          this._metricMap[targetNode.data.id].success_rate = 1;\n        }\n      }\n    }\n    return metrics;\n  }\n\n  private _getObjectFromArray(array: NodeTreeElement[], id: string) {\n    for (let i = 0; i < array.length; i++) {\n      if (array[i].node.data.label === id) {\n        return array[i];\n      }\n    }\n    return undefined;\n  }\n\n  private _addNode(nodeToAdd: NodeTreeElement, currentLayerNode: NodeTreeElement, currentLevel: number) {\n    const namespace = nodeToAdd.node.data.namespace;\n    const namespaceLength = namespace ? namespace.length : 0;\n    if (namespaceLength === currentLevel) {\n      const possibleDuplicate = _.find(currentLayerNode.children, function (o) {\n        return o.id === nodeToAdd.id;\n      });\n      if (possibleDuplicate) {\n        //Object.assign(possibleDuplicate.node.data.metrics, nodeToAdd.node.data.metrics);\n        //TODO: Copy/merge metrics\n      } else {\n        currentLayerNode.children.push(nodeToAdd);\n      }\n    } else {\n      const nextLayerNode = this._getObjectFromArray(\n        currentLayerNode.children,\n        nodeToAdd.node.data.namespace[currentLevel]\n      );\n      if (nextLayerNode === undefined) {\n        const children: NodeTreeElement[] = [];\n        const newNode = {\n          id: nodeToAdd.node.data.namespace[currentLevel],\n          children: children,\n          node: {\n            data: {\n              id: nodeToAdd.node.data.namespace[currentLevel],\n              type: EnGraphNodeType.PARENT,\n              label: nodeToAdd.node.data.namespace[currentLevel],\n              parent: nodeToAdd.node.data.namespace[currentLevel - 1],\n              namespace: this._getNameSpaceFromCurrentLevel(nodeToAdd.node.data.namespace, currentLevel),\n              layer: currentLevel,\n              metrics: {},\n            },\n          },\n        };\n        currentLayerNode.children.push(newNode);\n        currentLevel++;\n        newNode.node.data.metrics = this._sumMetrics(nodeToAdd.node, newNode.node);\n        this._addNode(nodeToAdd, newNode, currentLevel);\n      } else {\n        const nextTopLayerNode = this._getObjectFromArray(\n          currentLayerNode.children,\n          nodeToAdd.node.data.namespace[currentLevel]\n        );\n        nextTopLayerNode.node.data.type = EnGraphNodeType.PARENT;\n        nextTopLayerNode.node.data.metrics = this._sumMetrics(nodeToAdd.node, nextTopLayerNode.node);\n        currentLevel++;\n        this._addNode(nodeToAdd, nextTopLayerNode, currentLevel);\n      }\n    }\n  }\n}\n\nexport default NodeTree;\n"
  },
  {
    "path": "src/processing/pre_processor.ts",
    "content": "import { DataFrame } from '@grafana/data';\nimport _ from 'lodash';\nimport { PanelController } from '../panel/PanelController';\nimport { GraphDataElement, GraphDataType, CurrentData } from '../types';\n\nclass PreProcessor {\n  controller: PanelController;\n\n  constructor(controller: PanelController) {\n    this.controller = controller;\n  }\n\n  _transformObjects(data: any[]): GraphDataElement[] {\n    const {\n      aggregationType,\n      sourceColumn,\n      targetColumn,\n      extOrigin: externalSource,\n      extTarget: externalTarget,\n      namespaceDelimiter,\n    } = this.controller.getSettings(true).dataMapping;\n\n    const result = _.map(data, (dataObject) => {\n      let source = _.has(dataObject, sourceColumn) && dataObject[sourceColumn] !== '';\n      let target = _.has(dataObject, targetColumn) && dataObject[targetColumn] !== '';\n      const extSource = _.has(dataObject, externalSource) && dataObject[externalSource] !== '';\n      const extTarget = _.has(dataObject, externalTarget) && dataObject[externalTarget] !== '';\n\n      let trueCount = [source, target, extSource, extTarget].filter((e) => e).length;\n\n      if (trueCount > 1) {\n        if (target && extTarget) {\n          target = false;\n        } else if (source && extSource) {\n          source = false;\n        } else {\n          console.error('source-target conflict for data element', dataObject);\n          return undefined;\n        }\n      }\n\n      const result: GraphDataElement = {\n        target: '',\n        data: dataObject,\n        type: GraphDataType.INTERNAL,\n      };\n\n      if (_.has(dataObject, 'namespace')) {\n        const nameSpace = _.get(dataObject, 'namespace');\n        if (nameSpace) {\n          const namespaceResolved = nameSpace.split(namespaceDelimiter);\n          result.namespace = namespaceResolved;\n        }\n      }\n\n      if (trueCount === 0) {\n        result.target = dataObject[aggregationType];\n        result.type = GraphDataType.EXTERNAL_IN;\n      } else {\n        if (source || target) {\n          if (source) {\n            result.source = dataObject[sourceColumn];\n            result.target = dataObject[aggregationType];\n          } else {\n            result.source = dataObject[aggregationType];\n            result.target = dataObject[targetColumn];\n          }\n\n          if (result.source === result.target) {\n            result.type = GraphDataType.SELF;\n          }\n        } else if (extSource) {\n          result.source = dataObject[externalSource];\n          result.target = dataObject[aggregationType];\n          result.type = GraphDataType.EXTERNAL_IN;\n        } else if (extTarget) {\n          result.source = dataObject[aggregationType];\n          result.target = dataObject[externalTarget];\n          result.type = GraphDataType.EXTERNAL_OUT;\n        }\n      }\n      return result;\n    });\n\n    const filteredResult: GraphDataElement[] = result.filter(\n      (element): element is GraphDataElement => element !== null\n    );\n    return filteredResult;\n  }\n\n  _mergeGraphData(data: GraphDataElement[]): GraphDataElement[] {\n    const groupedData = _.values(_.groupBy(data, (element) => element.source + '<--->' + element.target));\n\n    const mergedData = _.map(groupedData, (group) => {\n      return _.reduce(group, (result, next) => {\n        return _.merge(result, next);\n      });\n    });\n\n    return mergedData;\n  }\n\n  _cleanMetaData(columnMapping: any, metaData: any) {\n    const result: any = {};\n\n    _.forOwn(columnMapping, (value, key) => {\n      if (_.has(metaData, value)) {\n        result[key] = metaData[value];\n      }\n    });\n\n    return result;\n  }\n\n  _extractColumnNames(data: GraphDataElement[]): string[] {\n    const columnNames: string[] = _(data)\n      .flatMap((dataElement) => _.keys(dataElement.data))\n      .uniq()\n      .sort()\n      .value();\n\n    return columnNames;\n  }\n\n  _getField(fieldName: string, fields: any[]) {\n    for (const field of fields) {\n      if (field.name === fieldName) {\n        return field;\n      }\n    }\n    return undefined;\n  }\n\n  _mergeSeries(series: any[]) {\n    let mergedSeries: any = undefined;\n    for (const seriesElement of series) {\n      if (mergedSeries === undefined) {\n        mergedSeries = seriesElement;\n      } else {\n        for (const field of seriesElement.fields) {\n          const mergedField = this._getField(field.name, mergedSeries.fields);\n          if (mergedField === undefined) {\n            mergedSeries.fields.push(field);\n          } else {\n            mergedField.values = _.concat(field.values, mergedField.values);\n          }\n        }\n      }\n    }\n    return mergedSeries;\n  }\n\n  _dataToRows(inputDataSets: any) {\n    let rows: any[] = [];\n\n    const {\n      aggregationType,\n      sourceColumn,\n      targetColumn,\n      namespaceColumn,\n      extOrigin,\n      extTarget,\n      type,\n      errorRateColumn,\n      errorRateOutgoingColumn,\n      responseTimeColumn,\n      responseTimeOutgoingColumn,\n      requestRateColumn,\n      requestRateOutgoingColumn,\n      baselineRtUpper,\n    } = this.controller.getSettings(true).dataMapping;\n\n    for (const inputData of inputDataSets) {\n      const { fields } = inputData;\n      const externalSourceField = _.find(fields, ['name', extOrigin]);\n      const externalTargetField = _.find(fields, ['name', extTarget]);\n      const aggregationSuffixField = _.find(fields, ['name', aggregationType]);\n\n      const typeField = _.find(fields, ['name', type]);\n\n      const sourceColumnField = _.find(fields, ['name', sourceColumn]);\n      const targetColumnField = _.find(fields, ['name', targetColumn]);\n      const namespaceColumnField = _.find(fields, ['name', namespaceColumn]);\n\n      const errorRateColumnField = _.find(fields, ['name', errorRateColumn]);\n      const errorRateOutgoingColumnField = _.find(fields, ['name', errorRateOutgoingColumn]);\n      const responseTimeColumnField = _.find(fields, ['name', responseTimeColumn]);\n      const responseTimeOutgoingColumnField = _.find(fields, ['name', responseTimeOutgoingColumn]);\n      const requestRateColumnField = _.find(fields, ['name', requestRateColumn]);\n      const requestRateOutgoingColumnField = _.find(fields, ['name', requestRateOutgoingColumn]);\n      const responseTimeBaselineField = _.find(fields, ['name', baselineRtUpper]);\n\n      for (let i = 0; i < inputData.length; i++) {\n        const row: any = {};\n        row[extOrigin] = externalSourceField?.values.get(i);\n        row[extTarget] = externalTargetField?.values.get(i);\n        row[aggregationType] = aggregationSuffixField?.values.get(i);\n        row[sourceColumn] = sourceColumnField?.values.get(i);\n        row[targetColumn] = targetColumnField?.values.get(i);\n        row['namespace'] = namespaceColumnField?.values.get(i);\n        row['error_rate_in'] = errorRateColumnField?.values.get(i);\n        row['error_rate_out'] = errorRateOutgoingColumnField?.values.get(i);\n        row['response_time_in'] = responseTimeColumnField?.values.get(i);\n        row['response_time_out'] = responseTimeOutgoingColumnField?.values.get(i);\n        row['rate_in'] = requestRateColumnField?.values.get(i);\n        row['rate_out'] = requestRateOutgoingColumnField?.values.get(i);\n        row['threshold'] = responseTimeBaselineField?.values.get(i);\n        row['type'] = typeField?.values.get(i);\n        // The above code returns { \"\": undefined } for values that do not exist.\n        // These values are filtered by this line.\n        Object.keys(row).forEach((key) => (row[key] === undefined || row[key] === '') && delete row[key]);\n        rows.push(row);\n      }\n    }\n    return rows;\n  }\n\n  _resolveData(row: any) {\n    let source = _.has(row, 'sourceColumn') && row['sourceColumn'] !== '';\n    let target = _.has(row, 'targetColumn') && row['targetColumn'] !== '';\n    const extSource = _.has(row, 'extOrigin') && row['extOrigin'] !== '';\n    const extTarget = _.has(row, 'extTarget') && row['extTarget'] !== '';\n    let trueCount = [source, target, extSource, extTarget].filter((e) => e).length;\n\n    if (trueCount > 1) {\n      if (target && extTarget) {\n        target = false;\n      } else if (source && extSource) {\n        source = false;\n      } else {\n        console.error('source-target conflict for data element', row);\n        return;\n      }\n    }\n    let resolvedObject: any = {\n      data: row.data,\n    };\n    if (trueCount === 0) {\n      resolvedObject.target = row['aggregationSuffix'];\n      resolvedObject.type = GraphDataType.EXTERNAL_IN;\n    } else {\n      if (source || target) {\n        if (source) {\n          resolvedObject.source = row['sourceColumn'];\n          resolvedObject.target = row['aggregationSuffix'];\n          resolvedObject.type = GraphDataType.INTERNAL;\n        } else {\n          resolvedObject.source = row['aggregationSuffix'];\n          resolvedObject.target = row['targetColumn'];\n          resolvedObject.type = GraphDataType.INTERNAL;\n        }\n\n        if (resolvedObject.source === resolvedObject.target) {\n          resolvedObject.type = GraphDataType.SELF;\n        }\n      } else if (extSource) {\n        resolvedObject.source = row['externalSource'];\n        resolvedObject.target = row['aggregationSuffix'];\n        resolvedObject.type = GraphDataType.EXTERNAL_IN;\n      } else if (extTarget) {\n        resolvedObject.source = row['aggregationSuffix'];\n        resolvedObject.target = row['externalTarget'];\n        resolvedObject.type = GraphDataType.EXTERNAL_OUT;\n      }\n    }\n    return resolvedObject;\n  }\n\n  _mergeObjects(rows: any[]) {\n    let mergedObjects: any[] = [];\n\n    for (const row of rows) {\n      mergedObjects.push(row);\n    }\n    return mergedObjects;\n  }\n\n  processData(inputData: DataFrame[]): CurrentData {\n    const rows = this._dataToRows(inputData);\n\n    const flattenData = this._mergeObjects(rows);\n\n    const graphElements = this._transformObjects(flattenData);\n\n    const columnNames = this._extractColumnNames(graphElements);\n\n    const mergedData = this._mergeGraphData(graphElements);\n\n    return {\n      graph: mergedData,\n      raw: inputData,\n      columnNames: columnNames,\n    };\n  }\n}\n\nexport default PreProcessor;\n"
  },
  {
    "path": "src/processing/utils/Utils.ts",
    "content": "import _ from 'lodash';\nimport { DataMapping } from '../../types';\nimport { ServiceDependencyGraph } from 'panel/serviceDependencyGraph/ServiceDependencyGraph';\n\nexport function isPresent<T>(t: T | undefined | null | void): t is T {\n  return t !== undefined && t !== null;\n}\n\nexport default {\n  getConfig: function (graph: ServiceDependencyGraph, configName: keyof DataMapping) {\n    return graph.getSettings(true).dataMapping[configName];\n  },\n};\n"
  },
  {
    "path": "src/types.tsx",
    "content": "import { DataFrame } from '@grafana/data';\n\nexport interface PanelSettings {\n  animate: boolean;\n  sumTimings: boolean;\n  filterEmptyConnections: boolean;\n  style: PanelStyleSettings;\n  showDebugInformation: boolean;\n  showConnectionStats: boolean;\n  icons: IconResource[];\n  externalIcons: IconResource[];\n  dataMapping: DataMapping;\n  drillDownLink: string;\n  showBaselines: boolean;\n  timeFormat: string;\n}\n\nexport interface DataMapping {\n  aggregationType: string;\n  sourceColumn: string;\n  targetColumn: string;\n  namespaceColumn: string;\n  namespaceDelimiter: string;\n\n  responseTimeColumn: string;\n  requestRateColumn: string;\n  errorRateColumn: string;\n  responseTimeOutgoingColumn: string;\n  requestRateOutgoingColumn: string;\n  errorRateOutgoingColumn: string;\n\n  extOrigin: string;\n  extTarget: string;\n  type: string;\n  showDummyData: boolean;\n\n  baselineRtUpper: string;\n}\n\nexport interface PanelStyleSettings {\n  healthyColor: string;\n  dangerColor: string;\n  noDataColor: string;\n}\n\nexport interface IconResource {\n  pattern: string;\n  filename: string;\n}\n\nexport interface QueryResponseColumn {\n  type?: string;\n  text: string;\n}\n\nexport interface CyData {\n  group: string;\n  data: {\n    id: string;\n    source?: string;\n    target?: string;\n    metrics: IntGraphMetrics;\n    type?: string;\n    external_type?: string;\n    layer?: number;\n    namespace?: string[];\n    parent?: string;\n  };\n}\n\nexport interface CurrentData {\n  graph: GraphDataElement[];\n  raw: DataFrame[];\n  columnNames: string[];\n}\n\nexport interface GraphDataElement {\n  source?: string;\n  target: string;\n  data: DataElement;\n  type: GraphDataType;\n  namespace?: string[];\n}\n\nexport interface DataElement {\n  rate_in?: number;\n  rate_out?: number;\n  response_time_in?: number;\n  response_time_out?: number;\n  error_rate_in?: number;\n  error_rate_out?: number;\n  type?: string;\n  threshold?: number;\n}\n\nexport enum GraphDataType {\n  SELF = 'SELF',\n  INTERNAL = 'INTERNAL',\n  EXTERNAL_OUT = 'EXTERNAL_OUT',\n  EXTERNAL_IN = 'EXTERNAL_IN',\n}\n\nexport interface IntGraph {\n  nodes: IntGraphNode[];\n  edges: IntGraphEdge[];\n}\n\nexport interface IntGraphNode {\n  data: IntGraphNodeData;\n}\n\nexport interface IntGraphNodeData {\n  id: string;\n  type: EnGraphNodeType;\n  metrics?: IntGraphMetrics;\n  external_type?: string;\n  label?: string;\n  layer: number;\n  namespace?: string[];\n  parent?: string;\n}\n\nexport interface IntGraphMetrics {\n  rate?: number;\n  error_rate?: number;\n  response_time?: number;\n  success_rate?: number;\n  threshold?: number;\n}\n\nexport enum EnGraphNodeType {\n  INTERNAL = 'INTERNAL',\n  EXTERNAL = 'EXTERNAL',\n  PARENT = 'PARENT',\n}\n\nexport interface IntGraphEdge {\n  source: string;\n  target: string;\n  data: IntGraphEdgeData;\n  metrics?: IntGraphMetrics;\n}\n\nexport interface IntGraphEdgeData {\n  source: string;\n  target: string;\n  metrics?: IntGraphMetrics;\n}\n\nexport interface NodeTreeElement {\n  id: string;\n  node?: IntGraphNode;\n  children: NodeTreeElement[];\n}\n\nexport interface Particle {\n  velocity: number;\n  startTime: number;\n}\n\nexport interface Particles {\n  normal: Particle[];\n  danger: Particle[];\n}\n\nexport interface TableContent {\n  name: string;\n  responseTime: string;\n  rate: string;\n  error: string;\n}\n\nexport interface IntSelectionStatistics {\n  requests?: number;\n  errors?: number;\n  responseTime?: number;\n  threshold?: number;\n  thresholdViolation?: boolean;\n}\n\nexport interface CyCanvas {\n  getCanvas: () => HTMLCanvasElement;\n  clear: (arg0: CanvasRenderingContext2D) => void;\n  resetTransform: (arg0: CanvasRenderingContext2D) => void;\n  setTransform: (arg0: CanvasRenderingContext2D) => void;\n}\n\nexport interface IntTableHeader {\n  text: string;\n  dataField: string;\n  isKey?: boolean;\n  sort: boolean;\n  headerClasses?: string;\n  footerClasses?: string;\n  classes?: string;\n  sortFunc?: (a: string, b: string, order: string, _dataField: any, _rowA: any) => number;\n  ignoreLiteral?: string;\n}\n\nexport interface NodeData {\n  name: string;\n  time: string;\n  requests: string;\n  error_rate: string;\n}\n\nexport interface ScaleValue {\n  unit: string;\n  factor: number;\n}\n\nexport interface DrawContext {\n  ctx: CanvasRenderingContext2D;\n  now: number;\n  xDirection: number;\n  yDirection: number;\n  xMinLimit: number;\n  xMaxLimit: number;\n  yMinLimit: number;\n  yMaxLimit: number;\n  sourcePoint: cytoscape.Position;\n}\n\nexport interface Rectangle {\n  coordinates: Point;\n  height: number;\n  width: number;\n}\n\nexport interface Point {\n  x: number;\n  y: number;\n}\n"
  },
  {
    "path": "src/typings/index.d.ts",
    "content": "declare module 'cytoscape-canvas';\ndeclare module 'cytoscape-cola';\ndeclare module 'human-format';\ndeclare module 'react-bootstrap-table-next';\ndeclare module 'react-icon-picker';\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"./.config/tsconfig.json\",\n  \"include\": [\"src\", \"types\"],\n  \"jsx\": \"react\",\n  \"compilerOptions\": {\n    \"strictNullChecks\": false,\n    \"rootDir\": \"./src\",\n    \"baseUrl\": \"./src\",\n    \"typeRoots\": [\n      \"./node_modules/@types\",\n      \"./typings\"\n    ]\n  }\n}\n"
  }
]