master bf97e1d64386 cached
74 files
190.4 KB
49.2k tokens
197 symbols
1 requests
Download .txt
Showing preview only (209K chars total). Download the full file or copy to clipboard to get everything.
Repository: NovatecConsulting/novatec-service-dependency-graph-panel
Branch: master
Commit: bf97e1d64386
Files: 74
Total size: 190.4 KB

Directory structure:
gitextract_lgiult8p/

├── .circleci/
│   └── config.yml
├── .codeclimate.yml
├── .config/
│   ├── .eslintrc
│   ├── .prettierrc.js
│   ├── Dockerfile
│   ├── README.md
│   ├── jest/
│   │   ├── mocks/
│   │   │   └── react-inlinesvg.tsx
│   │   └── utils.js
│   ├── jest-setup.js
│   ├── jest.config.js
│   ├── tsconfig.json
│   ├── types/
│   │   └── custom.d.ts
│   └── webpack/
│       ├── constants.ts
│       ├── utils.ts
│       └── webpack.config.ts
├── .editorconfig
├── .eslintrc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── feature_request.md
│   │   └── question.md
│   └── workflows/
│       └── build.yml
├── .gitignore
├── .nvmrc
├── .prettierrc.js
├── .release-it.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── appveyor.yml
├── docker-compose.yaml
├── docs/
│   └── README.md
├── jest-setup.js
├── jest.config.js
├── package.json
├── provisioning/
│   ├── dashboards/
│   │   └── dashboards.yml
│   └── home/
│       └── home.json
├── scripts/
│   └── create-signed-plugin.sh
├── src/
│   ├── assets/
│   │   └── icons/
│   │       └── icon_index.json
│   ├── css/
│   │   └── novatec-service-dependency-graph-panel.css
│   ├── dummy_data_frame.ts
│   ├── migration/
│   │   └── PanelMigration.tsx
│   ├── module.test.ts
│   ├── module.ts
│   ├── options/
│   │   ├── DefaultSettings.tsx
│   │   ├── TypeAheadTextfield/
│   │   │   ├── TypeaheadTextfield.css
│   │   │   └── TypeaheadTextfield.tsx
│   │   ├── dummyDataSwitch/
│   │   │   └── DummyDataSwitch.tsx
│   │   ├── iconMapping/
│   │   │   ├── IconMapping.css
│   │   │   └── IconMapping.tsx
│   │   └── options.tsx
│   ├── panel/
│   │   ├── PanelController.tsx
│   │   ├── asset_utils.tsx
│   │   ├── canvas/
│   │   │   ├── collision_detector.ts
│   │   │   ├── graph_canvas.ts
│   │   │   └── particle_engine.ts
│   │   ├── layout_options.ts
│   │   ├── serviceDependencyGraph/
│   │   │   ├── ServiceDependencyGraph.css
│   │   │   └── ServiceDependencyGraph.tsx
│   │   └── statistics/
│   │       ├── IncomingStatistics.tsx
│   │       ├── NodeStatistics.tsx
│   │       ├── SortableTable.tsx
│   │       ├── Statistics.css
│   │       ├── Statistics.tsx
│   │       └── utils/
│   │           └── Utils.ts
│   ├── plugin.json
│   ├── processing/
│   │   ├── graph_generator.ts
│   │   ├── node_substitutor.ts
│   │   ├── node_tree.ts
│   │   ├── pre_processor.ts
│   │   └── utils/
│   │       └── Utils.ts
│   ├── types.tsx
│   └── typings/
│       └── index.d.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .circleci/config.yml
================================================
version: 2.1

parameters:
  ssh-fingerprint:
    type: string
    default: ${GITHUB_SSH_FINGERPRINT}

aliases:
  # Workflow filters
  - &filter-only-master
    branches:
      only: master
  - &filter-only-release
    branches:
      only: /^v[1-9]*[0-9]+\.[1-9]*[0-9]+\.x$/

workflows:
  plugin_workflow:
    jobs:
      - build

executors:
  default_exec: # declares a reusable executor
    docker:
      - image: srclosson/grafana-plugin-ci-alpine:latest
  e2e_exec:
    docker:
      - image: srclosson/grafana-plugin-ci-e2e:latest

jobs:
  build:
    executor: default_exec
    steps:
      - checkout
      - restore_cache:
          name: restore node_modules
          keys:
          - build-cache-{.Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
      - run:
          name: Install dependencies
          command: |
            mkdir ci
            [ -f ~/project/node_modules/.bin/grafana-toolkit ] || yarn install --frozen-lockfile
      - save_cache:
          name: save node_modules
          paths:
            - ~/project/node_modules
          key: build-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
      - run:
          name: Build and test frontend
          command: ./node_modules/.bin/grafana-toolkit plugin:ci-build
      - run:
          name: Move results to ci folder
          command: ./node_modules/.bin/grafana-toolkit plugin:ci-build --finish
      - run:
          name: Package distribution
          command: |
            ./node_modules/.bin/grafana-toolkit plugin:ci-package
      - persist_to_workspace:
          root: .
          paths:
          - ci/jobs/package
          - ci/packages
          - ci/dist
          - ci/grafana-test-env
      - store_artifacts:
          path: ci


================================================
FILE: .codeclimate.yml
================================================
exclude_patterns:
- "dist"
- "**/node_modules/"
- "**/*.d.ts"
- "src/libs/"
- "src/images/"
- "src/img/"
- "src/screenshots/"


================================================
FILE: .config/.eslintrc
================================================
/*
 * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
 *
 * In order to extend the configuration follow the steps in
 * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-eslint-config
 */
{
  "extends": ["@grafana/eslint-config"],
  "root": true,
  "rules": {
    "react/prop-types": "off"
  }
}


================================================
FILE: .config/.prettierrc.js
================================================
/*
 * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
 *
 * In order to extend the configuration follow the steps in .config/README.md
 */

module.exports = {
  "endOfLine": "auto",
  "printWidth": 120,
  "trailingComma": "es5",
  "semi": true,
  "jsxSingleQuote": false,
  "singleQuote": true,
  "useTabs": false,
  "tabWidth": 2
};

================================================
FILE: .config/Dockerfile
================================================
ARG grafana_version=latest
ARG grafana_image=grafana-enterprise

FROM grafana/${grafana_image}:${grafana_version}

# Make it as simple as possible to access the grafana instance for development purposes
# Do NOT enable these settings in a public facing / production grafana instance
ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin"
ENV GF_AUTH_ANONYMOUS_ENABLED "true"
ENV GF_AUTH_BASIC_ENABLED "false"
# Set development mode so plugins can be loaded without the need to sign
ENV GF_DEFAULT_APP_MODE "development"

# Inject livereload script into grafana index.html
USER root
RUN sed -i 's/<\/body><\/html>/<script src=\"http:\/\/localhost:35729\/livereload.js\"><\/script><\/body><\/html>/g' /usr/share/grafana/public/views/index.html

================================================
FILE: .config/README.md
================================================
# Default build configuration by Grafana

**This is an auto-generated directory and is not intended to be changed! ⚠️**

The `.config/` directory holds basic configuration for the different tools
that are used to develop, test and build the project. In order to make it updates easier we ask you to
not edit files in this folder to extend configuration.

## How to extend the basic configs?

Bear in mind that you are doing it at your own risk, and that extending any of the basic configuration can lead
to issues around working with the project.

### Extending the ESLint config

Edit the `.eslintrc` file in the project root in order to extend the ESLint configuration.

**Example:**

```json
{
  "extends": "./.config/.eslintrc",
  "rules": {
    "react/prop-types": "off"
  }
}
```

---

### Extending the Prettier config

Edit the `.prettierrc.js` file in the project root in order to extend the Prettier configuration.

**Example:**

```javascript
module.exports = {
  // Prettier configuration provided by Grafana scaffolding
  ...require('./.config/.prettierrc.js'),

  semi: false,
};
```

---

### Extending the Jest config

There are two configuration in the project root that belong to Jest: `jest-setup.js` and `jest.config.js`.

**`jest-setup.js`:** A file that is run before each test file in the suite is executed. We are using it to
set up the Jest DOM for the testing library and to apply some polyfills. ([link to Jest docs](https://jestjs.io/docs/configuration#setupfilesafterenv-array))

**`jest.config.js`:** The main Jest configuration file that extends the Grafana recommended setup. ([link to Jest docs](https://jestjs.io/docs/configuration))

#### ESM errors with Jest

A 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:

```javascript
process.env.TZ = 'UTC';
const { grafanaESModules, nodeModulesToTransform } = require('./config/jest/utils');

module.exports = {
  // Jest configuration provided by Grafana
  ...require('./.config/jest.config'),
  // Inform jest to only transform specific node_module packages.
  transformIgnorePatterns: [nodeModulesToTransform([...grafanaESModules, 'packageName'])],
};
```

---

### Extending the TypeScript config

Edit the `tsconfig.json` file in the project root in order to extend the TypeScript configuration.

**Example:**

```json
{
  "extends": "./.config/tsconfig.json",
  "compilerOptions": {
    "preserveConstEnums": true
  }
}
```

---

### Extending the Webpack config

Follow these steps to extend the basic Webpack configuration that lives under `.config/`:

#### 1. Create a new Webpack configuration file

Create a new config file that is going to extend the basic one provided by Grafana.
It can live in the project root, e.g. `webpack.config.ts`.

#### 2. Merge the basic config provided by Grafana and your custom setup

We are going to use [`webpack-merge`](https://github.com/survivejs/webpack-merge) for this.

```typescript
// webpack.config.ts
import type { Configuration } from 'webpack';
import { merge } from 'webpack-merge';
import grafanaConfig from './.config/webpack/webpack.config';

const config = async (env): Promise<Configuration> => {
  const baseConfig = await grafanaConfig(env);

  return merge(baseConfig, {
    // Add custom config here...
    output: {
      asyncChunks: true,
    },
  });
};

export default config;
```

#### 3. Update the `package.json` to use the new Webpack config

We need to update the `scripts` in the `package.json` to use the extended Webpack configuration.

**Update for `build`:**

```diff
-"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
+"build": "webpack -c ./webpack.config.ts --env production",
```

**Update for `dev`:**

```diff
-"dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development",
+"dev": "webpack -w -c ./webpack.config.ts --env development",
```

### Configure grafana image to use when running docker

By 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`. 

**Example:**

```yaml
version: '3.7'

services:
  grafana:
    container_name: 'myorg-basic-app'
    build:
      context: ./.config
      args:
        grafana_version: ${GRAFANA_VERSION:-9.1.2}
        grafana_image: ${GRAFANA_IMAGE:-grafana}
```

In 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.

---


================================================
FILE: .config/jest/mocks/react-inlinesvg.tsx
================================================
// Due to the grafana/ui Icon component making fetch requests to
// `/public/img/icon/<icon_name>.svg` we need to mock react-inlinesvg to prevent
// the failed fetch requests from displaying errors in console.

import React from 'react';

type Callback = (...args: any[]) => void;

export interface StorageItem {
  content: string;
  queue: Callback[];
  status: string;
}

export const cacheStore: { [key: string]: StorageItem } = Object.create(null);

const SVG_FILE_NAME_REGEX = /(.+)\/(.+)\.svg$/;

const InlineSVG = ({ src }: { src: string }) => {
  // testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`)
  const testId = src.replace(SVG_FILE_NAME_REGEX, '$2');
  return <svg xmlns="http://www.w3.org/2000/svg" data-testid={testId} viewBox="0 0 24 24" />;
};

export default InlineSVG;


================================================
FILE: .config/jest/utils.js
================================================
/*
 * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
 *
 * In order to extend the configuration follow the steps in .config/README.md
 */

/*
 * This utility function is useful in combination with jest `transformIgnorePatterns` config
 * to transform specific packages (e.g.ES modules) in a projects node_modules folder.
 */
const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!.*(${moduleNames.join('|')})\/.*)`;

// Array of known nested grafana package dependencies that only bundle an ESM version
const grafanaESModules = [
  '.pnpm', // Support using pnpm symlinked packages
  '@grafana/schema',
  'd3',
  'd3-color',
  'd3-force',
  'd3-interpolate',
  'd3-scale-chromatic',
  'ol',
  'react-colorful',
  'rxjs',
  'uuid',
];

module.exports = {
  nodeModulesToTransform,
  grafanaESModules,
};


================================================
FILE: .config/jest-setup.js
================================================
/*
 * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
 *
 * In order to extend the configuration follow the steps in
 * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config
 */

import '@testing-library/jest-dom';

// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(global, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation((query) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(), // deprecated
    removeListener: jest.fn(), // deprecated
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

HTMLCanvasElement.prototype.getContext = () => {};


================================================
FILE: .config/jest.config.js
================================================
/*
 * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
 *
 * In order to extend the configuration follow the steps in
 * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config
 */

const path = require('path');
const { grafanaESModules, nodeModulesToTransform } = require('./jest/utils');

module.exports = {
  moduleNameMapper: {
    '\\.(css|scss|sass)$': 'identity-obj-proxy',
    'react-inlinesvg': path.resolve(__dirname, 'jest', 'mocks', 'react-inlinesvg.tsx'),
  },
  modulePaths: ['<rootDir>/src'],
  setupFilesAfterEnv: ['<rootDir>/jest-setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  testMatch: [
    '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
    '<rootDir>/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}',
    '<rootDir>/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}',
  ],
  transform: {
    '^.+\\.(t|j)sx?$': [
      '@swc/jest',
      {
        sourceMaps: 'inline',
        jsc: {
          parser: {
            syntax: 'typescript',
            tsx: true,
            decorators: false,
            dynamicImport: true,
          },
        },
      },
    ],
  },
  // Jest will throw `Cannot use import statement outside module` if it tries to load an
  // ES module without it being transformed first. ./config/README.md#esm-errors-with-jest
  transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)],
};


================================================
FILE: .config/tsconfig.json
================================================
/*
 * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
 *
 * In order to extend the configuration follow the steps in
 * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-typescript-config
 */
 {
  "compilerOptions": {
    "alwaysStrict": true,
    "declaration": false,
    "rootDir": "../src",
    "baseUrl": "../src",
    "typeRoots": ["../node_modules/@types"],
    "resolveJsonModule": true
  },
  "ts-node": {
    "compilerOptions": {
      "module": "commonjs",
      "target": "es5",
      "esModuleInterop": true
    },
    "transpileOnly": true
  },
  "include": ["../src", "./types"],
  "extends": "@grafana/tsconfig"
}


================================================
FILE: .config/types/custom.d.ts
================================================
// Image declarations
declare module '*.gif' {
  const src: string;
  export default src;
}

declare module '*.jpg' {
  const src: string;
  export default src;
}

declare module '*.jpeg' {
  const src: string;
  export default src;
}

declare module '*.png' {
  const src: string;
  export default src;
}

declare module '*.webp' {
  const src: string;
  export default src;
}

declare module '*.svg' {
  const content: string;
  export default content;
}

// Font declarations
declare module '*.woff';
declare module '*.woff2';
declare module '*.eot';
declare module '*.ttf';
declare module '*.otf';


================================================
FILE: .config/webpack/constants.ts
================================================
export const SOURCE_DIR = 'src';
export const DIST_DIR = 'dist';


================================================
FILE: .config/webpack/utils.ts
================================================
import fs from 'fs';
import process from 'process';
import os from 'os';
import path from 'path';
import util from 'util';
import { glob } from 'glob';
import { SOURCE_DIR } from './constants';


export function isWSL() {
  if (process.platform !== 'linux') {
    return false;
  }

  if (os.release().toLowerCase().includes('microsoft')) {
    return true;
  }

  try {
    return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft');
  } catch {
    return false;
  }
} 

export function getPackageJson() {
  return require(path.resolve(process.cwd(), 'package.json'));
}

export function getPluginJson() {
  return require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`));
}

export function hasReadme() {
  return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md'));
}

// Support bundling nested plugins by finding all plugin.json files in src directory
// then checking for a sibling module.[jt]sx? file.
export async function getEntries(): Promise<Record<string, string>> {
  const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true });

  const plugins = await Promise.all(pluginsJson.map((pluginJson) => {
      const folder = path.dirname(pluginJson);
      return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true });
    })
  );

  return plugins.reduce((result, modules) => {
    return modules.reduce((result, module) => {
      const pluginPath = path.dirname(module);
      const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, '');
      const entryName = pluginName === '' ? 'module' : `${pluginName}/module`;

      result[entryName] = module;
      return result;
    }, result);
  }, {});
}


================================================
FILE: .config/webpack/webpack.config.ts
================================================
/*
 * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
 *
 * In order to extend the configuration follow the steps in
 * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-webpack-config
 */

import CopyWebpackPlugin from 'copy-webpack-plugin';
import ESLintPlugin from 'eslint-webpack-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import LiveReloadPlugin from 'webpack-livereload-plugin';
import path from 'path';
import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';
import { Configuration } from 'webpack';

import { getPackageJson, getPluginJson, hasReadme, getEntries, isWSL } from './utils';
import { SOURCE_DIR, DIST_DIR } from './constants';

const pluginJson = getPluginJson();

const config = async (env): Promise<Configuration> => {
  const baseConfig: Configuration = {
    cache: {
      type: 'filesystem',
      buildDependencies: {
        config: [__filename],
      },
    },

    context: path.join(process.cwd(), SOURCE_DIR),

    devtool: env.production ? 'source-map' : 'eval-source-map',

    entry: await getEntries(),

    externals: [
      'lodash',
      'jquery',
      'moment',
      'slate',
      'emotion',
      '@emotion/react',
      '@emotion/css',
      'prismjs',
      'slate-plain-serializer',
      '@grafana/slate-react',
      'react',
      'react-dom',
      'react-redux',
      'redux',
      'rxjs',
      'react-router',
      'react-router-dom',
      'd3',
      'angular',
      '@grafana/ui',
      '@grafana/runtime',
      '@grafana/data',

      // Mark legacy SDK imports as external if their name starts with the "grafana/" prefix
      ({ request }, callback) => {
        const prefix = 'grafana/';
        const hasPrefix = (request) => request.indexOf(prefix) === 0;
        const stripPrefix = (request) => request.substr(prefix.length);

        if (hasPrefix(request)) {
          return callback(undefined, stripPrefix(request));
        }

        callback();
      },
    ],

    mode: env.production ? 'production' : 'development',

    module: {
      rules: [
        {
          exclude: /(node_modules)/,
          test: /\.[tj]sx?$/,
          use: {
            loader: 'swc-loader',
            options: {
              jsc: {
                baseUrl: './src',
                target: 'es2015',
                loose: false,
                parser: {
                  syntax: 'typescript',
                  tsx: true,
                  decorators: false,
                  dynamicImport: true,
                },
              },
            },
          },
        },
        {
          test: /\.css$/,
          use: ["style-loader", "css-loader"]
        },
        {
          test: /\.s[ac]ss$/,
          use: ['style-loader', 'css-loader', 'sass-loader'],
        },
        {
          test: /\.(png|jpe?g|gif|svg)$/,
          type: 'asset/resource',
          generator: {
            // Keep publicPath relative for host.com/grafana/ deployments
            publicPath: `public/plugins/${pluginJson.id}/img/`,
            outputPath: 'img/',
            filename: Boolean(env.production) ? '[hash][ext]' : '[name][ext]',
          },
        },
        {
          test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/,
          type: 'asset/resource',
          generator: {
            // Keep publicPath relative for host.com/grafana/ deployments
            publicPath: `public/plugins/${pluginJson.id}/fonts/`,
            outputPath: 'fonts/',
            filename: Boolean(env.production) ? '[hash][ext]' : '[name][ext]',
          },
        },
      ],
    },

    output: {
      clean: {
        keep: new RegExp(`(.*?_(amd64|arm(64)?)(.exe)?|go_plugin_build_manifest)`),
      },
      filename: '[name].js',
      library: {
        type: 'amd',
      },
      path: path.resolve(process.cwd(), DIST_DIR),
      publicPath: '/',
    },

    plugins: [
      new CopyWebpackPlugin({
        patterns: [
          // If src/README.md exists use it; otherwise the root README
          // To `compiler.options.output`
          { from: hasReadme() ? 'README.md' : '../README.md', to: '.', force: true },
          { from: 'plugin.json', to: '.' },
          { from: '../LICENSE', to: '.' },
          { from: '../CHANGELOG.md', to: '.', force: true },
          { from: '**/*.json', to: '.' }, // TODO<Add an error for checking the basic structure of the repo>
          { from: '**/*.svg', to: '.', noErrorOnMissing: true }, // Optional
          { from: '**/*.png', to: '.', noErrorOnMissing: true }, // Optional
          { from: '**/*.html', to: '.', noErrorOnMissing: true }, // Optional
          { from: 'img/**/*', to: '.', noErrorOnMissing: true }, // Optional
          { from: 'libs/**/*', to: '.', noErrorOnMissing: true }, // Optional
          { from: 'static/**/*', to: '.', noErrorOnMissing: true }, // Optional
        ],
      }),
      // Replace certain template-variables in the README and plugin.json
      new ReplaceInFileWebpackPlugin([
        {
          dir: DIST_DIR,
          files: ['plugin.json', 'README.md'],
          rules: [
            {
              search: /\%VERSION\%/g,
              replace: getPackageJson().version,
            },
            {
              search: /\%TODAY\%/g,
              replace: new Date().toISOString().substring(0, 10),
            },
            {
              search: /\%PLUGIN_ID\%/g,
              replace: pluginJson.id,
            },
          ],
        },
      ]),
      new ForkTsCheckerWebpackPlugin({
        async: Boolean(env.development),
        issue: {
          include: [{ file: '**/*.{ts,tsx}' }],
        },
        typescript: { configFile: path.join(process.cwd(), 'tsconfig.json') },
      }),
      new ESLintPlugin({
        extensions: ['.ts', '.tsx'],
        lintDirtyModulesOnly: Boolean(env.development), // don't lint on start, only lint changed files
      }),
      ...(env.development ? [new LiveReloadPlugin()] : []),
    ],

    resolve: {
      extensions: ['.js', '.jsx', '.ts', '.tsx'],
      // handle resolving "rootDir" paths
      modules: [path.resolve(process.cwd(), 'src'), 'node_modules'],
      unsafeCache: true,
    },
  }

  if(isWSL()) {
    baseConfig.watchOptions = {
      poll: 3000,
      ignored: /node_modules/,
    }}


  return baseConfig;

};

export default config;


================================================
FILE: .editorconfig
================================================
# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

[*.{js,ts,tsx,scss}]
quote_type = single

[*.md]
trim_trailing_whitespace = false


================================================
FILE: .eslintrc
================================================
{
  "extends": "./.config/.eslintrc"
}

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] - *ENTER TITLE*"
labels: bug
assignees: ''

---

Please provide as much and as detailed information as possible as it will make it easier for us to reproduce and fix the bug!

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
A clear and concise description of how to reproduce the bug.

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Your Setup:**
- OS Grafana is running on:
- OS & Browser from which Grafana is accessed:
- Plugin-Version:
- Grafana-Version:
- Datasource & Version:
- Other: 

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] - *ENTER TITLE*"
labels: enhancement
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/ISSUE_TEMPLATE/question.md
================================================
---
name: Question
about: Ask a question about the project.
title: "[Question] - *ENTER TITLE*"
labels: question
assignees: ''

---

**Write your question here**

**Screenshots**

**Where and how could we improve the readme?**


================================================
FILE: .github/workflows/build.yml
================================================
name: Build plugin

on:
  push:

jobs:
  build:
    runs-on: ubuntu-latest
    container:
      image: node:20.19
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Build plugin
        run: |
          yarn install && yarn build


================================================
FILE: .gitignore
================================================
node_modules
npm-debug.log
*.log
.vscode
dist
coverage

================================================
FILE: .nvmrc
================================================
16

================================================
FILE: .prettierrc.js
================================================
module.exports = {
  // Prettier configuration provided by Grafana scaffolding
  ...require("./.config/.prettierrc.js")
};

================================================
FILE: .release-it.json
================================================
{
    "github": {
      "release": false
    },
    "npm": {
      "publish": false
    },
    "hooks": {
      "after:bump":
        "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"
    }
  }

================================================
FILE: CHANGELOG.md
================================================
# Change Log

All notable changes to this project will be documented in this file.

## v4.2.0

Update dependencies

## v4.1.2

Add layering mechanism
Add collision between edge-labels
Update dependencies

## v4.0.2

Bug fix in icon path


## v4.0.1

SumTimings now working as expected
Response Time Health now displayed in node statistics
External Icons now use the correct path


## v4.0.0

Ported project to react.
aggregationType is not needed as template variable anymore.
Unit type of data now can be chosen.
Tables of Incoming/Outgoing Statistics are now sortable.
Settings needed for the dummy data to be displayed is now filled in automatically when dummy data is activated.
Service Icons can now be customized for both Internal and External Services.


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

## Releasing

To create a new release of the plugin, follow these steps.

You may also read the [official documentation](https://grafana.com/developers/plugin-tools/publish-a-plugin/publish-a-plugin).

### 1. Push a new tag

Push the current state of the plugin to the remote GitHub repository.

  `git tag <version>`

  `git tag push origin -tag <version>`

### 2. Create a release

Create a new release from the pushed tag manually in GitHub. 
Add appropriate release notes.

### 3. Add the signed plugin to the release

To sign a plugin, you will need a **plugin signing token**, 
which has been created in our Grafana Cloud account.

Set the environment variable `GRAFANA_ACCESS_POLICY_TOKEN` via

`export GRAFANA_ACCESS_POLICY_TOKEN=<token>` 

or in Windows 

`set GRAFANA_ACCESS_POLICY_TOKEN=<token>`

Then run the release script to create a signed plugin:

`./scripts/create-signed-plugin.sh`

Add the created zip file in the `/release` directory to the GitHub release.

### 4. Submit the plugin in Grafana

If you want to publish the release in the Grafana marketplace, you will have to submit the
release in our Grafana Cloud account.

You will need to provide:

- URL of the plugin (link to the zip file of the release)
- SHA1 of the plugin (SHA1 hash of the zip file)
- Source code URL (URL of the repository for the release tag)
- Test description (use the `docker-compose.yml` to run the plugin with dummy data)


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   Copyright 2025 Novatec Consulting GmbH

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
## Novatec Service Dependency Graph Panel

![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)
[![License](https://img.shields.io/github/license/NovatecConsulting/novatec-service-dependency-graph-panel)](LICENSE)


![SDG_PRESENTATION](https://user-images.githubusercontent.com/53812669/173822816-da6791ec-c785-435b-a235-21ead3ebd4e1.gif)


**Version 4.2.0 is only compatible with Grafana from version 10.4.0!**

**Version 4.0.0 is only compatible with Grafana from version 7.1.0!**



The Service Dependency Graph Panel by [Novatec](https://www.novatec-gmbh.de/en/) provides you with many features such as monitoring 
your latencies, errors and requests of your desired service. This interactive panel for [Grafana](https://grafana.com/) will help you
visualize the processes of your application much better. 


### Updating the Service Dependency Graph Panel
The 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!

___

## Configuration of the Data Source

### Using Static Dummy Data

If 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.

The 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.

Depending on the query result, the data provides the following tags:
* **service**: The service (application) the data is related to.
* **namespace**: The namespace of a service. Every literal divided by "." corresponds to one level of a namespace. For instance **demo.infrastructure**.
* **protocol**: The communication type (e.g. HTTP, JMS, ...).
* **origin_service**: In case of an incoming communication, this is the origin service.
* **target_service**: In case of an outgoing communication, this is the target service.
* **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).
* **target_external**: The target of an outgoing communication, which cannot be correlated to a known service (e.g. third party HTTP endpoint).

Depending on the query result, the data provides the following fields:
* **in_timesum**: The total sum of all incoming request response times. (Prometheus style)
* **in_count**: The total amount of incoming requests.
* **error_in**: The amount of incoming requests which produced an error.
* **out_timesum**: The total sum of all outgoing request response times. (Prometheus style)
* **out_count**: The total amount of outgoing requests.
* **error_out**: The amount of outgoing requests which produced an error.
* **threshold**: The critical threshold in milliseconds for the response times of incoming requests.


In 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.
After activating the Dummy Data your Data Mapping should look like this:

| key | value |
| --- | --- |
| Response Time | in_timesum |
| Request Rate  | in_count |
| Error Rate    | error_in |
| Response Time (Outgoing) | out_timesum |
| Request Rate (Outgoing) | out_count |
| Error Rate (Outgoing) | error_out |
| Response Time Baseline (Upper) | threshold |


_Note that you may have to refresh the dashboard or reload the page in order for it to work._

##### Live example dummy data

Downloading 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. 
Just open the docker images' Grafana and choose the dashboard `Service Graph` to see the fully functional Service Dependency Graph.
___

### Use your own Data Source

If 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:

* 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.
 
* The data can contain multiple value columns. These columns have to be mapped on specific attributes using the panel's `Data Mappings` options. 
**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.

#### Examples

##### Example 1

If the previously described requirements are respected, a minimal table can be as follows:

| app | target_app | req_rate |
| --- | --- | --- | 
| service a | service b | 50 | 
| service a | service c | 75 |
| service c | service d | 25 |

Assuming the panel's settings are specified as seen in the screenshot, the panel will visualize the data as following:

![Visualization of the minimal data table.](https://raw.githubusercontent.com/NovatecConsulting/novatec-service-dependency-graph-panel/master/src/img/data-example-1.png)

> Note: It is important to know that connections can only be generated if at least one request-rate column (incoming or outgoing) is defined.

##### Example 2

In 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).

| app | target_app | req_rate | resp_time |
| --- | --- | --- | --- | 
| service a | service b | 50 | 4000 |
| service a | service c | 75 | 13650 |
| service c | service d | 25 | 750 |

Now, 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.

![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)

___

## Service Icons

The service dependency graph plugin allows you to display your own symbols in the drawn nodes.
For this purpose the option 'Service Icon Mapping' can be used.
Here you can specify an assignment of icons to certain name patterns.
All nodes that match the specified pattern (regular expression) will get the icon.

![Custom service icons in the graph.](https://raw.githubusercontent.com/NovatecConsulting/novatec-service-dependency-graph-panel/master/src/img/service-icons.png)

##### Example

A sample assignment is included by default: `Pattern: java // Icon: java`.
This means that all nodes which have `java` in their name get the `java` icon.

#### Custom Service Icons

You can add custom icons, by putting them into the plugin's `/assets/icons/` directory.
The file type **has to be `PNG`** and the icon itself and **has to be square**.
In 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.

##### Example

If the `icon_index.json` has the following content:

```
["java", "star_trek"]
```

it is assumed that the files `java.png` and `star_trek.png` is existing in the `/assets/icons/` directory.
___

### Tracing Drilldown

The service dependency graph plugin allows you to specify a backend URL for each drawn node.
For this purpose the option 'Tracing Drilldown' can be used.
Here you can specify a backend URL. An open and closed curly bracket `{}` is the placeholder for the selected node.
Each node will get an arrow icon in the details view. This icon is a link to your backend, specified in the options.
The curly brackets `{}` will be replaced with the selected node.

#### Example

`http://{}/my/awesome/path` will end up to `http://customers-service/my/awesome/path` when you select the `customers-service`.

___

### Layering

From version 4.1.0, the Service Dependency Graph Panel supports layering service nodes by their respective namespace. 

####  Setup
To 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. 

#### Usage
You 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. 
___

## Create a Release

To create a release bundle, ensure `release-it` is installed:
```
npm install --global release-it
```
To build a release bundle:
```
release-it [--no-git.requireCleanWorkingDir]
```

### Found a bug? Have a question? Wanting to contribute?

Feel 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.


================================================
FILE: appveyor.yml
================================================
# Test against the latest version of this Node.js version
environment:
  nodejs_version: "12"

# Local NPM Modules
cache:
  - node_modules

# Install scripts. (runs after repo cloning)
install:
  # Get the latest stable version of Node.js or io.js
  - ps: Install-Product node $env:nodejs_version
  # install modules
  - npm install -g yarn --quiet
  - yarn install --pure-lockfile

# Post-install test scripts.
test_script:
  # Output useful info for debugging.
  - node --version
  - npm --version

# Run the build
build_script:
  - yarn dev   # This will also run prettier!
  - yarn build # make sure both scripts work


================================================
FILE: docker-compose.yaml
================================================
services:
  grafana:
    container_name: 'novatec-sdg-panel'
    platform: "linux/amd64"
    build:
      context: ./.config
      args:
        grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise}
        grafana_version: ${GRAFANA_VERSION:-11.6.0}
    environment:
      - GF_PATHS_PROVISIONING=/usr/share/grafana/custom/
    ports:
      - 3000:3000/tcp
    volumes:
      - ./dist:/var/lib/grafana/plugins/novatec-sdg-panel
      - ./provisioning:/usr/share/grafana/custom/
      - ./provisioning/home/home.json:/usr/share/grafana/public/dashboards/home.json


================================================
FILE: docs/README.md
================================================
TODO: add example docs structure


================================================
FILE: jest-setup.js
================================================
// Jest setup provided by Grafana scaffolding
import './.config/jest-setup';


================================================
FILE: jest.config.js
================================================
// force timezone to UTC to allow tests to work regardless of local timezone
// generally used by snapshots, but can affect specific tests
process.env.TZ = 'UTC';

module.exports = {
  // Jest configuration provided by Grafana scaffolding
  ...require('./.config/jest.config'),
};


================================================
FILE: package.json
================================================
{
  "name": "novatec-service-dependency-graph-panel",
  "version": "4.2.0",
  "description": "Service Dependency Graph panel for Grafana",
  "main": "src/module.js",
  "scripts": {
    "build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
    "dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development",
    "e2e": "yarn exec cypress install && yarn exec grafana-e2e run",
    "e2e:update": "yarn exec cypress install && yarn exec grafana-e2e run --update-screenshots",
    "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .",
    "lint:fix": "yarn run lint --fix",
    "server": "docker-compose up --build",
    "sign": "npx --yes @grafana/sign-plugin@latest",
    "test": "jest --watch --onlyChanged",
    "test:ci": "jest --passWithNoTests --maxWorkers 4",
    "typecheck": "tsc --noEmit"
  },
  "keywords": [
    "grafana",
    "plugin",
    "service-dependency-graph",
    "topology"
  ],
  "repository": "github:grafana/NovatecConsulting/novatec-service-dependency-graph-panel",
  "author": "Novatec",
  "license": "Apache-2.0",
  "bugs": {
    "url": "https://github.com/NovatecConsulting/novatec-service-dependency-graph-panel/issues",
    "email": "plugins@grafana.com"
  },
  "devDependencies": {
    "@babel/core": "^7.26.10",
    "@babel/preset-env": "^7.23.9",
    "@grafana/e2e": "^10.1.8",
    "@grafana/e2e-selectors": "^10.4.0",
    "@grafana/eslint-config": "^6.0.0",
    "@grafana/tsconfig": "^1.2.0-rc1",
    "@swc/core": "1.3.75",
    "@swc/helpers": "^0.5.15",
    "@swc/jest": "^0.2.37",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^16.2.0",
    "@types/jest": "^29.5.14",
    "@types/lodash": "^4.17.16",
    "@types/node": "^20.11.17",
    "@types/react-cytoscapejs": "^1.2.5",
    "copy-webpack-plugin": "^11.0.0",
    "css-loader": "^6.7.3",
    "emotion": "10.0.27",
    "eslint-webpack-plugin": "^4.2.0",
    "fork-ts-checker-webpack-plugin": "^8.0.0",
    "glob": "^10.4.5",
    "identity-obj-proxy": "3.0.0",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "prettier": "^3.5.3",
    "replace-in-file-webpack-plugin": "^1.0.6",
    "sass": "1.63.2",
    "sass-loader": "13.3.1",
    "style-loader": "3.3.3",
    "swc-loader": "^0.2.6",
    "ts-jest": "^28.0.4",
    "ts-node": "^10.9.2",
    "tsconfig-paths": "^4.2.0",
    "typescript": "^5.3.3",
    "webpack": "^5.94.0",
    "webpack-cli": "^6.0.1",
    "webpack-livereload-plugin": "^3.0.2"
  },
  "engines": {
    "node": ">=20"
  },
  "dependencies": {
    "@emotion/css": "^11.13.5",
    "@grafana/data": "^10.4.0",
    "@grafana/runtime": "^10.4.0",
    "@grafana/ui": "^10.4.0",
    "@types/react-autosuggest": "^10.1.11",
    "@types/react-bootstrap-table-next": "^4.0.26",
    "babel-preset-react": "^6.24.1",
    "cytoscape": "^3.31.1",
    "cytoscape-canvas": "^3.0.1",
    "cytoscape-cola": "^2.5.1",
    "human-format": "^0.11.0",
    "react": "^18.3.1",
    "react-autosuggest": "^10.1.0",
    "react-bootstrap-table-next": "^4.0.3",
    "react-cytoscapejs": "^2.0.0",
    "react-dom": "^18.3.1",
    "react-native": "^0.78.1"
  },
  "packageManager": "yarn@1.22.22",
  "resolutions": {
    "underscore": "^1.12.1",
    "semver": "^6.3.1"
  }
}


================================================
FILE: provisioning/dashboards/dashboards.yml
================================================
apiVersion: 1

providers:
  - name: 'default'
    orgId: 1
    folder: ''
    type: file
    disableDeletion: true
    allowUiUpdates: true
    updateIntervalSeconds: 10 #how often Grafana will scan for changed dashboards
    options:
      path: /usr/share/grafana/custom/dashboards


================================================
FILE: provisioning/home/home.json
================================================
{
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": {
          "type": "grafana",
          "uid": "-- Grafana --"
        },
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "id": 1,
  "links": [],
  "panels": [
    {
      "datasource": {
        "type": "datasource",
        "uid": "grafana"
      },
      "gridPos": {
        "h": 17,
        "w": 24,
        "x": 0,
        "y": 0
      },
      "id": 1,
      "options": {
        "dataMapping": {
          "aggregationType": "service",
          "baselineRtUpper": "threshold",
          "errorRateColumn": "error_in",
          "errorRateOutgoingColumn": "error_out",
          "extOrigin": "origin_external",
          "extTarget": "target_external",
          "requestRateColumn": "in_count",
          "requestRateOutgoingColumn": "out_count",
          "responseTimeColumn": "in_timesum",
          "responseTimeOutgoingColumn": "out_timesum",
          "showDummyData": true,
          "sourceColumn": "origin_service",
          "targetColumn": "target_service",
          "type": "protocol"
        },
        "drillDownLink": "",
        "externalIcons": [
          {
            "filename": "web",
            "pattern": "web"
          },
          {
            "filename": "message",
            "pattern": "jms"
          },
          {
            "filename": "database",
            "pattern": "jdbc"
          },
          {
            "filename": "http",
            "pattern": "http"
          }
        ],
        "filterEmptyConnections": true,
        "icons": [
          {
            "filename": "java",
            "pattern": "java"
          },
          {
            "filename": "star_trek",
            "pattern": "spok|star trek"
          }
        ],
        "showBaselines": false,
        "showConnectionStats": true,
        "showDebugInformation": false,
        "style": {
          "dangerColor": "rgb(196, 22, 42)",
          "healthyColor": "rgb(87, 148, 242)",
          "noDataColor": "rgb(123, 123, 138)"
        },
        "sumTimings": true,
        "timeFormat": "m"
      },
      "pluginVersion": "4.2.0",
      "title": "SDG",
      "type": "novatec-sdg-panel"
    },
    {
      "gridPos": {
        "h": 9,
        "w": 24,
        "x": 0,
        "y": 17
      },
      "id": 123125,
      "type": "gettingstarted"
    },
    {
      "gridPos": {
        "h": 9,
        "w": 24,
        "x": 0,
        "y": 26
      },
      "id": 123124,
      "type": "gettingstarted"
    },
    {
      "gridPos": {
        "h": 9,
        "w": 24,
        "x": 0,
        "y": 35
      },
      "id": 123123,
      "type": "gettingstarted"
    }
  ],
  "schemaVersion": 39,
  "tags": [],
  "templating": {
    "list": []
  },
  "time": {
    "from": "now-1h",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "browser",
  "title": "Dummy Dashboard",
  "version": 1,
  "weekStart": ""
}


================================================
FILE: scripts/create-signed-plugin.sh
================================================
#!/bin/bash

echo Building plugin...
yarn install && yarn build

echo Signing plugin...
yarn sign

echo Creating zip file...
cp -r dist novatec-sdg-panel
mkdir -p release && zip -r release/novatec-sdg-panel.zip novatec-sdg-panel
rm -r novatec-sdg-panel


================================================
FILE: src/assets/icons/icon_index.json
================================================
["java", "star_trek", "balancer", "database", "default", "ftp", "http", "ldap", "mainframe", "message", "smtp", "web"]


================================================
FILE: src/css/novatec-service-dependency-graph-panel.css
================================================
.service-dependency-graph-panel {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
}

.service-dependency-graph-panel .graph-container {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: row;
}

.service-dependency-graph-panel .service-dependency-graph {
  position: relative;
  flex-grow: 1;
  min-width: 0;
}

.service-dependency-graph-panel .canvas-container {
   width: 100%;
   height: 100%;
   overflow: hidden;
}

.service-dependency-graph-panel .zoom-button-container {
  position: absolute;
  top: 0;
  right: 1rem;
  z-index: 99;
  width: 35px;
}

.service-dependency-graph-panel .statistics {
  flex-basis: 0;
  transition: flex-basis 250ms ease-in-out;
  overflow-y: scroll;

}

.service-dependency-graph-panel .statistics.show {
  flex-basis: 30rem;
  padding-left: 0.5%;
}

.service-dependency-graph-panel .header--selection {
  font-size: 1.25em;
  text-align: center;
  border-bottom: 2px solid #161719;
  font-weight: 500;
  color: rgb(216, 217, 218);
}

.service-dependency-graph-panel .secondHeader--selection {
  font-size: 1.2em;
  text-align: center;
  padding-top: 1.5rem;
  padding-bottom: 0.5rem;
}

.service-dependency-graph-panel .no-data--selection{
  color: #888888;
  text-align: center;
}

.service-dependency-graph-panel .table--selection {
  width: 99%;
  table-layout: fixed;
}

.service-dependency-graph-panel .table--selection th, .table--selection td {
  padding: 3px 5px;
}

.service-dependency-graph-panel .table--selection tr {
  border-bottom: 2px solid #161719;
}

.service-dependency-graph-panel .table--td--selection {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.service-dependency-graph-panel .threshold--bad {
  color:#f2495c;
}

.service-dependency-graph-panel .threshold--good {
  color: #73bf69;
}

.service-dependency-graph-panel .table--th--selectionSmall {
  width: 5.5rem;
}

.service-dependency-graph-panel .table--th--selectionMedium {
  width: 8rem;
}

.service-dependency-graph-panel .table--selection--head {
  background-color: #28282a;
  border-top: 2px solid #161719;
  color: #33b5e5;
}

.service-dependency-graph-panel .width-100 {
  width: 100%;
}


================================================
FILE: src/dummy_data_frame.ts
================================================
import { ArrayVector, DataFrame, FieldType } from '@grafana/data';

const data: DataFrame[] = [
  {
    refId: 'A',
    name: undefined,
    meta: undefined,
    fields: [
      {
        name: 'time',
        type: FieldType.time,
        config: {},
        values: new ArrayVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
      },
      {
        name: 'origin_external',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          'tcp://localhost:61616',
          'tcp://10.10.10.10:61616',
        ]),
      },
      {
        name: 'origin_service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          '',
          '',
          '',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'customers-service',
          'vets-service',
          'visits-service',
          'vets-service',
          '',
        ]),
      },
      {
        name: 'protocol',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'jms',
          'jms',
        ]),
      },
      {
        name: 'service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'api-gateway',
          'config-server',
          'discovery-server',
          'api-gateway',
          'customers-service',
          'discovery-server',
          'vets-service',
          'visits-service',
          'discovery-server',
          'discovery-server',
          'discovery-server',
          'visits-service',
          'api-gateway',
        ]),
      },
      {
        name: 'namespace',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'demo.infrastructure',
          'demo.infrastructure',
          'demo.infrastructure',
          'demo.infrastructure',
          'demo.domain-logic',
          'demo.infrastructure',
          'demo.domain-logic',
          'demo.domain-logic',
          'demo.infrastructure',
          'demo.infrastructure',
          'demo.infrastructure',
          'demo.domain-logic',
          'demo.infrastructure',
        ]),
      },
      {
        name: 'in_count',
        type: FieldType.number,
        config: {},
        values: new ArrayVector([508, 0, 0, 100, 347, 20, 63, 100, 20, 20, 20, 300, 300]),
      },
    ],
    length: 13,
  },
  {
    refId: 'B',
    name: undefined,
    meta: undefined,
    fields: [
      {
        name: 'time',
        type: FieldType.time,
        config: {},
        values: new ArrayVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
      },
      {
        name: 'protocol',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'jms',
          'http',
          'jdbc',
          'jdbc',
          'jdbc',
        ]),
      },
      {
        name: 'service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'config-server',
          'customers-service',
          'vets-service',
          'vets-service',
          'visits-service',
          'customers-service',
          'vets-service',
          'visits-service',
        ]),
      },
      {
        name: 'namespace',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          '',
          '',
          '',
          '',
          '',
          'web',
          'web',
          '',
          '',
          '',
          '',
          'demo.database',
          'demo.database',
          'demo.database',
        ]),
      },
      {
        name: 'target_external',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          '',
          '',
          '',
          '',
          '',
          '7a8dce897616:8080',
          'github.com',
          '',
          '',
          'tcp://localhost:61616',
          '',
          'jdbc:hsqldb:mem:testdb',
          'jdbc:hsqldb:mem:testdb',
          'jdbc:hsqldb:mem:testdb',
        ]),
      },
      {
        name: 'target_service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'api-gateway',
          'customers-service',
          'discovery-server',
          'vets-service',
          'visits-service',
          '',
          '',
          'discovery-server',
          'discovery-server',
          'visits-service',
          'discovery-server',
          '',
          '',
          '',
        ]),
      },
      {
        name: 'out_count',
        type: FieldType.number,
        config: {},
        values: new ArrayVector([100, 347, 20, 62, 100, 0, 0, 20, 20, 300, 20, 1847, 441, 100]),
      },
    ],
    length: 14,
  },
  {
    refId: 'C',
    name: undefined,
    meta: undefined,
    fields: [
      { name: 'time', type: FieldType.time, config: {}, values: new ArrayVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) },
      {
        name: 'origin_service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          '',
          '',
          '',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'customers-service',
          'vets-service',
          'visits-service',
        ]),
      },
      {
        name: 'protocol',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
        ]),
      },
      {
        name: 'service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'api-gateway',
          'config-server',
          'discovery-server',
          'api-gateway',
          'customers-service',
          'discovery-server',
          'vets-service',
          'visits-service',
          'discovery-server',
          'discovery-server',
          'discovery-server',
        ]),
      },
      {
        name: 'namespace',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'demo.infrastructure',
          'demo.infrastructure',
          'demo.infrastructure',
          'demo.infrastructure',
          'demo.domain-logic',
          'demo.infrastructure',
          'demo.domain-logic',
          'demo.domain-logic',
          'demo.domain-logic',
          'demo.domain-logic',
          'demo.infrastructure',
          'demo.infrastructure',
          'demo.infrastructure',
        ]),
      },
      {
        name: 'target_external',
        type: FieldType.string,
        config: {},
        values: new ArrayVector(['', '', '', '', '', '', '', '', '', '', '']),
      },
      {
        name: 'in_timesum',
        type: FieldType.number,
        config: {},
        values: new ArrayVector([
          45140.008427999986, 0, 0, 1511.9842349999872, 819.3634589999965, 21.881731999999943, 281.0465210000002,
          325.85070300000007, 21.53124399999996, 21.40604300000001, 20.813048000000038,
        ]),
      },
    ],
    length: 11,
  },
  {
    refId: 'D',
    name: undefined,
    meta: undefined,
    fields: [
      {
        name: 'time',
        type: FieldType.time,
        config: {},
        values: new ArrayVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
      },
      {
        name: 'protocol',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'http',
          'jdbc',
          'jdbc',
          'jdbc',
        ]),
      },
      {
        name: 'service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'api-gateway',
          'config-server',
          'customers-service',
          'vets-service',
          'visits-service',
          'customers-service',
          'vets-service',
          'visits-service',
        ]),
      },
      {
        name: 'namespace',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          '',
          '',
          '',
          '',
          '',
          'web',
          'web',
          '',
          '',
          '',
          'demo.database',
          'demo.database',
          'demo.database',
        ]),
      },
      {
        name: 'target_external',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          '',
          '',
          '',
          '',
          '',
          '7a8dce897616:8080',
          'github.com',
          '',
          '',
          '',
          'jdbc:hsqldb:mem:testdb',
          'jdbc:hsqldb:mem:testdb',
          'jdbc:hsqldb:mem:testdb',
        ]),
      },
      {
        name: 'target_service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'api-gateway',
          'customers-service',
          'discovery-server',
          'vets-service',
          'visits-service',
          '',
          '',
          'discovery-server',
          'discovery-server',
          'discovery-server',
          '',
          '',
          '',
        ]),
      },
      {
        name: 'out_timesum',
        type: FieldType.number,
        config: {},
        values: new ArrayVector([
          1700.468872999987, 1481.533606999972, 540.746261, 501.65547400000014, 394.81158100000175, 0, 0,
          84.59527999999978, 381.87400800000023, 225.65933600000017, 35.9093940000007, 13.000189000000091,
          12.258137999999946,
        ]),
      },
    ],
    length: 13,
  },
  {
    refId: 'E',
    name: undefined,
    meta: undefined,
    fields: [
      { name: 'time', type: FieldType.time, config: {}, values: new ArrayVector([0, 0, 0, 0]) },
      { name: 'origin_service', type: FieldType.string, config: {}, values: new ArrayVector(['', '', '', '']) },
      {
        name: 'protocol',
        type: FieldType.string,
        config: {},
        values: new ArrayVector(['http', 'http', 'http', 'http']),
      },
      {
        name: 'service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector(['api-gateway', 'discovery-server', 'customers-service', 'vets-service']),
      },
      {
        name: 'namespace',
        type: FieldType.string,
        config: {},
        values: new ArrayVector([
          'demo.infrastructure',
          'demo.infrastructure',
          'demo.domain-logic',
          'demo.domain-logic',
        ]),
      },
      { name: 'target_external', type: FieldType.string, config: {}, values: new ArrayVector(['', '', '', '']) },
      { name: 'error_in', type: FieldType.number, config: {}, values: new ArrayVector([14, 20, 20, 0]) },
    ],
    length: 4,
  },
  {
    refId: 'F',
    name: undefined,
    meta: undefined,
    fields: [
      { name: 'time', type: FieldType.time, config: {}, values: new ArrayVector([0, 0, 0, 0]) },
      {
        name: 'origin_service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector(['api-gateway', 'api-gateway', 'api-gateway', 'customers-service']),
      },
      {
        name: 'namespace',
        type: FieldType.string,
        config: {},
        values: new ArrayVector(['demo.domain-logic', 'demo.domain-logic', 'demo.domain-logic', 'demo.infrastructure']),
      },
      {
        name: 'protocol',
        type: FieldType.string,
        config: {},
        values: new ArrayVector(['http', 'http', 'http', 'http']),
      },
      {
        name: 'service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector(['customers-service', 'vets-service', 'visits-service', 'discovery-server']),
      },
      { name: 'target_external', type: FieldType.string, config: {}, values: new ArrayVector(['', '', '', '']) },
      { name: 'error_out', type: FieldType.number, config: {}, values: new ArrayVector([14, 0, 0, 20]) },
    ],
    length: 4,
  },
  {
    refId: 'G',
    name: undefined,
    meta: undefined,
    fields: [
      { name: 'time', type: FieldType.time, config: {}, values: new ArrayVector([0, 0]) },
      {
        name: 'service',
        type: FieldType.string,
        config: {},
        values: new ArrayVector(['api-gateway', 'customers-service']),
      },
      {
        name: 'namespace',
        type: FieldType.string,
        config: {},
        values: new ArrayVector(['demo.infrastructure', 'demo.domain-logic']),
      },
      { name: 'threshold', type: FieldType.number, config: {}, values: new ArrayVector([40.40604300000001, 10]) },
    ],
    length: 2,
  },
];

export default data;


================================================
FILE: src/migration/PanelMigration.tsx
================================================
import { PanelModel } from '@grafana/data';
import { DefaultSettings } from 'options/DefaultSettings';
import { PanelSettings } from 'types';

/**
 * Checks if the given options are in the format of version < 4.0.0.
 * @param options The options object which should be checked.
 */
function isLegacyFormat(options: any) {
  return options && !('showDummyData' in options['dataMapping']);
}

/**
 * Migrates the legacy iconMapping format to the iconMapping format of version > 4.0.0.
 * @param iconMappings The iconMappings object to be migrated.
 */
function migrateIconMapping(iconMappings: any) {
  const migratedIconMapping = [];
  for (const iconMapping of iconMappings) {
    migratedIconMapping.push({
      pattern: iconMapping.name,
      filename: iconMapping.filename,
    });
  }
  return migratedIconMapping;
}

/**
 * Migrates the legacy panel settings from version < 4.0.0 to the new format introduced in version 4.0.0
 * The newly introduced variable aggregationType will be set to $aggregationTyoe in order to ensure functionality with
 * the legacy setup of the panel.
 * All other newly added options will be set to their respective default values.
 * @param panel The panel object which should be migrated.
 */
export const PanelMigrationHandler = (panel: PanelModel<Partial<PanelSettings>> | any) => {
  const { settings } = panel;
  if (isLegacyFormat(settings)) {
    return {
      animate: settings.animate,
      sumTimings: settings.sumTimings,
      filterEmptyConnections: settings.filterEmptyConnections,
      style: {
        healthyColor: settings.style.healthyColor,
        dangerColor: settings.style.dangerColor,
        noDataColor: settings.style.unknownColor,
      },
      showDebugInformation: settings.showDebugInformation,
      showConnectionStats: settings.showConnectionStats,
      externalIcons: migrateIconMapping(settings.externalIcons),
      icons: settings.serviceIcons,
      dataMapping: {
        aggregationType: '$aggregationType',
        sourceColumn: settings.dataMapping.sourceComponentPrefix + '$aggregationType',
        targetColumn: settings.dataMapping.targetComponentPrefix + '$aggregationType',

        responseTimeColumn: settings.dataMapping.responseTimeColumn,
        requestRateColumn: settings.dataMapping.requestRateColumn,
        errorRateColumn: settings.dataMapping.errorRateColumn,
        responseTimeOutgoingColumn: settings.dataMapping.responseTimeOutgoingColumn,
        requestRateOutgoingColumn: settings.dataMapping.requestRateOutgoingColumn,
        errorRateOutgoingColumn: settings.dataMapping.errorRateOutgoingColumn,

        extOrigin: settings.dataMapping.extOrigin,
        extTarget: settings.dataMapping.extTarget,
        type: settings.dataMapping.type,
        showDummyData: settings.showDummyData,

        baselineRtUpper: settings.dataMapping.baselineRtUpper,
      },
      drillDownLink: settings.drillDownLink,
      showBaselines: settings.showBaselines,
      timeFormat: DefaultSettings.timeFormat,
    };
  }
  return settings;
};


================================================
FILE: src/module.test.ts
================================================
// Just a stub test
describe('placeholder test', () => {
  it('should return true', () => {
    expect(true).toBeTruthy();
  });
});


================================================
FILE: src/module.ts
================================================
import { PanelPlugin } from '@grafana/data';
import { PanelSettings } from './types';

import { PanelController } from './panel/PanelController';
import { optionsBuilder } from './options/options';
import { PanelMigrationHandler } from './migration/PanelMigration';

export const plugin = new PanelPlugin<PanelSettings>(PanelController)
  .setPanelOptions(optionsBuilder)
  .setMigrationHandler(PanelMigrationHandler);


================================================
FILE: src/options/DefaultSettings.tsx
================================================
import { PanelSettings } from '../types';

export const DefaultSettings: PanelSettings = {
  animate: true,

  dataMapping: {
    aggregationType: 'service',
    sourceColumn: 'origin_service',
    targetColumn: 'target_service',
    namespaceColumn: 'namespace',
    namespaceDelimiter: '.',

    responseTimeColumn: 'response-time',
    requestRateColumn: 'request-rate',
    errorRateColumn: 'error-rate',
    responseTimeOutgoingColumn: 'response-time-out',
    requestRateOutgoingColumn: 'request-rate-out',
    errorRateOutgoingColumn: 'error-rate-out',

    extOrigin: 'external_origin',
    extTarget: 'external_target',
    type: 'type',

    baselineRtUpper: 'threshold',

    showDummyData: false,
  },

  sumTimings: true,
  filterEmptyConnections: true,
  showDebugInformation: false,
  showConnectionStats: true,
  showBaselines: false,

  style: {
    healthyColor: 'rgb(87, 148, 242)',
    dangerColor: 'rgb(196, 22, 42)',
    noDataColor: 'rgb(123, 123, 138)',
  },

  icons: [
    {
      pattern: 'java',
      filename: 'java',
    },
    {
      pattern: 'spok|star trek',
      filename: 'star_trek',
    },
  ],

  externalIcons: [
    {
      pattern: 'web',
      filename: 'web',
    },
    {
      pattern: 'jms',
      filename: 'message',
    },
    {
      pattern: 'jdbc',
      filename: 'database',
    },
    {
      pattern: 'http',
      filename: 'http',
    },
  ],

  drillDownLink: '',
  timeFormat: 'm',
};


================================================
FILE: src/options/TypeAheadTextfield/TypeaheadTextfield.css
================================================
.service-dependency-graph-panel .suggestion {
    width: 100%;
    border-right: 1px solid #3865AB ;
    border-left: 1px solid #3865AB ;
    background-color: #0B0C0E;
    padding-left: 10px;
}

.service-dependency-graph-panel ul {
    list-style-type: none;
}

.service-dependency-graph-panel ul:last-child {
    border-bottom: 1px solid #3865AB ;
}


================================================
FILE: src/options/TypeAheadTextfield/TypeaheadTextfield.tsx
================================================
import React from 'react';
import Autosuggest, { InputProps } from 'react-autosuggest';
import { StandardEditorContext, StandardEditorProps } from '@grafana/data';
import './TypeaheadTextfield.css';
import { PanelSettings } from '../../types';
interface Props extends StandardEditorProps<string, PanelSettings> {
  item: any;
  value: string;
  onChange: (value?: string) => void;
  context: StandardEditorContext<any>;
}
interface State {
  item: any;
  value: string;
  onChange: (value?: string) => void;
  context: StandardEditorContext<any>;
  suggestions: string[];
}
export class TypeaheadTextField extends React.PureComponent<Props, State> {
  constructor(props: Props | Readonly<Props>) {
    super(props);
    let { value } = props;
    if (value === undefined) {
      value = props.item.defaultValue;
    }
    this.state = {
      ...props,
      value: value,
      suggestions: [],
    };
  }
  renderSuggestion(suggestion: string) {
    return <div>{suggestion}</div>;
  }
  getColumnNames() {
    let { data } = this.props.context;
    let series;
    let columnNames = [];
    if (data !== undefined && data.length > 0) {
      series = data[0].fields;
      for (const index in series) {
        const field = series[index];
        const { config, name } = field;
        if (config !== undefined && config.displayName !== undefined) {
          columnNames.push(config.displayName);
        } else {
          columnNames.push(name);
        }
      }
    }
    return columnNames;
  }
  onChange = (event: React.FormEvent<HTMLElement>, { newValue }: { newValue: string }) => {
    //TODO make this type nicer!
    const { path } = this.props.item;
    const { value } = event.currentTarget as HTMLInputElement;
    this.setState({
      value: value,
    });
    this.props.onChange.call(path, newValue);
  };
  getSuggestions = (value: string) => {
    let inputValue = '';
    if (value !== undefined) {
      return [];
    }
    if (value !== undefined && value !== null && value !== '') {
      inputValue = value.trim().toLowerCase();
    }
    const inputLength = inputValue.length;
    if (inputLength === 0 || inputValue === undefined) {
      return [];
    }
    return this.getColumnNames().filter((columnName) => columnName.toLowerCase().startsWith(inputValue));
  };
  onSuggestionsFetchRequested = (value: any) => {
    this.setState({
      suggestions: this.getSuggestions(value),
    });
  };
  getSuggestionValue = (suggestion: string) => {
    return suggestion;
  };
  onSuggestionsClearRequested = () => {
    this.setState({
      suggestions: [],
    });
  };
  render() {
    let { value } = this.props;
    if (value === undefined) {
      value = this.props.item.defaultValue;
    }
    const suggestions = this.getSuggestions(value);
    const inputProps: InputProps<string> = {
      placeholder: 'Enter column name...',
      value,
      onChange: this.onChange,
    };
    return (
      <Autosuggest
        suggestions={suggestions}
        onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
        onSuggestionsClearRequested={this.onSuggestionsClearRequested}
        getSuggestionValue={this.getSuggestionValue}
        renderSuggestion={this.renderSuggestion}
        inputProps={inputProps}
        theme={{
          input: 'input-small gf-form-input width-100',
          suggestion: 'suggestion',
        }}
      />
    );
  }
}


================================================
FILE: src/options/dummyDataSwitch/DummyDataSwitch.tsx
================================================
import React from 'react';
import { StandardEditorContext, StandardEditorProps } from '@grafana/data';
import { PanelSettings, DataMapping } from '../../types';
import { Switch } from '@grafana/ui';

interface Props extends StandardEditorProps<boolean, PanelSettings> {
  item: any;
  value: boolean;
  onChange: (value?: boolean) => void;
  context: StandardEditorContext<any>;
}

interface State {
  item: any;
  value: boolean;
  dataMapping: DataMapping | undefined;
  onChange: (value?: boolean) => void;
  context: StandardEditorContext<any>;
}

export class DummyDataSwitch extends React.PureComponent<Props, State> {
  constructor(props: Props | Readonly<Props>) {
    super(props);

    let { dataMapping } = props.context.options;
    if (dataMapping === undefined) {
      dataMapping = props.item.defaultValue;
    }
    this.state = {
      dataMapping: dataMapping,
      ...props,
    };
  }

  getDummyDataMapping = () => {
    return {
      aggregationType: 'service',
      sourceColumn: 'origin_service',
      targetColumn: 'target_service',
      responseTimeColumn: 'in_timesum',
      requestRateColumn: 'in_count',
      errorRateColumn: 'error_in',
      responseTimeOutgoingColumn: 'out_timesum',
      requestRateOutgoingColumn: 'out_count',
      errorRateOutgoingColumn: 'error_out',
      extOrigin: 'origin_external',
      extTarget: 'target_external',
      type: 'protocol',
      showDummyData: true,
      baselineRtUpper: 'threshold',
    };
  };

  onChange = () => {
    let { dataMapping } = this.props.context.options;
    const { item } = this.state;
    const { onChange } = this.props;
    const newValue = !dataMapping.showDummyData;

    if (newValue) {
      this.setState({ dataMapping: dataMapping });
      dataMapping = this.getDummyDataMapping();
    }
    dataMapping.showDummyData = newValue;
    onChange.call(item.path, dataMapping);
  };

  render() {
    let { dataMapping } = this.props.context.options;
    if (dataMapping === undefined) {
      dataMapping = this.props.item.defaultValue;
      this.props.context.options.dataMapping = this.props.item.defaultValue;
    }

    return (
      <div>
        <Switch value={dataMapping.showDummyData} onChange={() => this.onChange()} />
      </div>
    );
  }
}


================================================
FILE: src/options/iconMapping/IconMapping.css
================================================
.service-dependency-graph-panel .no-background {
    background-color: transparent;
}

.service-dependency-graph-panel .no-padding-left {
    padding-left: 0;
}

.service-dependency-graph-panel .icon-mapping-button {
    width: 46.5%;
    margin-left: 0;
}

.service-dependency-graph-panel .width-100 {
    width: 100%;
}

.service-dependency-graph-panel .width-half {
    width: 47%;
    text-overflow: ellipsis;
}


================================================
FILE: src/options/iconMapping/IconMapping.tsx
================================================
import React, { ChangeEvent } from 'react';
import { StandardEditorContext, StandardEditorProps } from '@grafana/data';
import { IconResource, PanelSettings } from '../../types';
import assetUtils from '../../panel/asset_utils';
import './IconMapping.css';

interface Props extends StandardEditorProps<string, PanelSettings> {
  item: any;
  value: any;
  onChange: (value?: string) => void;
  context: StandardEditorContext<any>;
}

interface State {
  item: any;
  value: string;
  onChange: (value?: string) => void;
  context: StandardEditorContext<any>;
  icons: string[];
}

export class IconMapping extends React.PureComponent<Props, State> {
  constructor(props: Props | Readonly<Props>) {
    super(props);
    this.state = {
      ...props,
      icons: [],
    };
    fetch(assetUtils.getAssetUrl('icon_index.json'))
      .then((response) => response.json())
      .then((data) => {
        data.sort();
        this.setState({
          icons: data,
        });
      })
      .catch(() => {
        console.error(
          'Could not load service icons mapping index. Please verify the "icon_index.json" in the plugin\'s asset directory.'
        );
      });
  }

  addMapping() {
    const { path } = this.state.item;
    const icons = this.state.context.options[path];
    icons.push({ pattern: 'my-type', filename: 'default' });
    this.state.onChange.call(path, icons);
  }

  removeMapping(index: number) {
    const { path } = this.state.item;
    const icons = this.state.context.options[path];
    icons.splice(index, 1);
    this.state.onChange.call(path, icons);
  }

  setPatternValue(event: React.ChangeEvent<HTMLInputElement>, index: number) {
    const { path } = this.state.item;
    const icons = this.state.context.options[path];
    icons[index].pattern = event.currentTarget.value;
    this.state.onChange.call(path, icons);
  }

  setFileNameValue(event: ChangeEvent<HTMLSelectElement>, index: number) {
    const { path } = this.state.item;
    const icons = this.state.context.options[path];
    icons[index].filename = event.currentTarget.value.toString();
    this.props.onChange.call(path, icons);
  }

  render() {
    const { path } = this.state.item;
    const { icons: iconNames } = this.state;
    let icons = this.state.context.options[path];
    if (icons === undefined) {
      icons = this.state.item.defaultValue;
      const context = this.state.context;
      context.options[path] = this.state.item.defaultValue;
      this.setState({
        context: context,
      });
    }

    return (
      <div>
        <div className="gf-form-inline">
          <div className="gf-form width-100">
            <label className="gf-form-label no-background no-padding-left width-half">Target Name (RegEx)</label>
            <label className="gf-form-label no-background no-padding-left width-half">Icon</label>
          </div>
        </div>
        <div>
          {icons.map((icon: IconResource, index: number) => (
            <>
              <div className="gf-form">
                <input
                  type="text"
                  className="input-small gf-form-input"
                  value={icon.pattern}
                  onChange={(e) => this.setPatternValue(e, index)}
                />

                <select
                  className="input-small gf-form-input"
                  value={icon.filename}
                  onChange={(e) => this.setFileNameValue(e, index)}
                >
                  {iconNames.map((iconName: string, index: number) => (
                    <option key={iconName + '-' + index} value={iconName}>
                      {iconName}
                    </option>
                  ))}
                </select>

                <a className="gf-form-label tight-form-func no-background" onClick={() => this.removeMapping(index)}>
                  <i className="fa fa-trash"></i>
                </a>
              </div>
            </>
          ))}
        </div>
        <button
          className="btn navbar-button navbar-button--primary icon-mapping-button"
          onClick={() => this.addMapping()}
        >
          Add Icon Mapping
        </button>
      </div>
    );
  }
}


================================================
FILE: src/options/options.tsx
================================================
import { PanelOptionsEditorBuilder } from '@grafana/data';
import { PanelSettings } from '../types';
import { TypeaheadTextField } from './TypeAheadTextfield/TypeaheadTextfield';
import { IconMapping } from './iconMapping/IconMapping';
import { DummyDataSwitch } from './dummyDataSwitch/DummyDataSwitch';
import { DefaultSettings } from './DefaultSettings';

export const optionsBuilder = (builder: PanelOptionsEditorBuilder<PanelSettings>) => {
  return (
    builder

      //Connection Mapping
      .addCustomEditor({
        path: 'dataMapping.aggregationType',
        id: 'aggregationType',
        editor: TypeaheadTextField,
        name: 'Component Column',
        category: ['Connection Mapping'],
        defaultValue: DefaultSettings.dataMapping.aggregationType,
      })

      .addCustomEditor({
        path: 'dataMapping.sourceColumn',
        id: 'sourceComponentPrefix',
        editor: TypeaheadTextField,
        name: 'Source Component Column',
        category: ['Connection Mapping'],
        defaultValue: DefaultSettings.dataMapping.sourceColumn,
      })

      .addCustomEditor({
        path: 'dataMapping.targetColumn',
        id: 'targetComponentPrefix',
        name: 'Target Component Column',
        category: ['Connection Mapping'],
        editor: TypeaheadTextField,
        defaultValue: DefaultSettings.dataMapping.targetColumn,
      })

      .addCustomEditor({
        path: 'dataMapping.namespaceColumn',
        id: 'namespaceColumn',
        name: 'Namespace Column',
        category: ['Connection Mapping'],
        editor: TypeaheadTextField,
        defaultValue: DefaultSettings.dataMapping.namespaceColumn,
      })

      .addCustomEditor({
        path: 'dataMapping.nameSpaceDelimiter',
        id: 'nameSpaceDelimiter',
        name: 'Namespace Delimiter',
        category: ['Connection Mapping'],
        editor: TypeaheadTextField,
        defaultValue: DefaultSettings.dataMapping.namespaceDelimiter,
      })

      .addCustomEditor({
        path: 'dataMapping.type',
        id: 'type',
        name: 'Type',
        category: ['Connection Mapping'],
        editor: TypeaheadTextField,
        defaultValue: DefaultSettings.dataMapping.type,
      })

      .addCustomEditor({
        path: 'dataMapping.extOrigin',
        id: 'externalOrigin',
        name: 'External Origin',
        category: ['Connection Mapping'],
        editor: TypeaheadTextField,
        defaultValue: DefaultSettings.dataMapping.extOrigin,
      })

      .addCustomEditor({
        path: 'dataMapping.extTarget',
        id: 'externalTarget',
        name: 'External Target',
        category: ['Connection Mapping'],
        editor: TypeaheadTextField,
        defaultValue: DefaultSettings.dataMapping.extTarget,
      })

      //Data Mapping
      .addCustomEditor({
        id: 'responseTime',
        path: 'dataMapping.responseTimeColumn',
        name: 'Response Time Column',
        editor: TypeaheadTextField,
        category: ['Data Mapping'],
        defaultValue: DefaultSettings.dataMapping.responseTimeColumn,
      })

      .addCustomEditor({
        id: 'requestRateColumn',
        path: 'dataMapping.requestRateColumn',
        name: 'Request Rate Column',
        editor: TypeaheadTextField,
        category: ['Data Mapping'],
        defaultValue: DefaultSettings.dataMapping.requestRateColumn,
      })

      .addCustomEditor({
        id: 'errorRateColumn',
        path: 'dataMapping.errorRateColumn',
        name: 'Error Rate Column',
        editor: TypeaheadTextField,
        category: ['Data Mapping'],
        defaultValue: DefaultSettings.dataMapping.errorRateColumn,
      })

      .addCustomEditor({
        id: 'responseTimeOutgoingColumn',
        path: 'dataMapping.responseTimeOutgoingColumn',
        name: 'Response Time Column (Outgoing)',
        editor: TypeaheadTextField,
        category: ['Data Mapping'],
        defaultValue: DefaultSettings.dataMapping.responseTimeOutgoingColumn,
      })

      .addCustomEditor({
        id: 'requestRateOutgoingColumn',
        path: 'dataMapping.requestRateOutgoingColumn',
        name: 'Request Rate Column (Outgoing)',
        editor: TypeaheadTextField,
        category: ['Data Mapping'],
        defaultValue: DefaultSettings.dataMapping.requestRateOutgoingColumn,
      })

      .addCustomEditor({
        id: 'errorRateOutgoingColumn',
        path: 'dataMapping.errorRateOutgoingColumn',
        name: 'Error Rate Column (Outgoing)',
        editor: TypeaheadTextField,
        category: ['Data Mapping'],
        defaultValue: DefaultSettings.dataMapping.errorRateOutgoingColumn,
      })

      .addCustomEditor({
        id: 'baselineRtUpper',
        path: 'dataMapping.baselineRtUpper',
        name: 'Response Time Baseline (Upper)',
        editor: TypeaheadTextField,
        category: ['Data Mapping'],
        defaultValue: DefaultSettings.dataMapping.baselineRtUpper,
      })

      //General Settings
      .addBooleanSwitch({
        path: 'showConnectionStats',
        name: 'Show Connection Statistics',
        category: ['General Settings'],
        defaultValue: DefaultSettings.showConnectionStats,
      })

      .addBooleanSwitch({
        path: 'sumTimings',
        name: 'Handle Timings as Sums',
        description:
          'If this setting is active, the timings provided' +
          'by the mapped response time columns are considered as a ' +
          'continually increasing sum of response times. When ' +
          'deactivated, it is considered that the timings provided ' +
          'by columns are the actual average response times.',
        category: ['General Settings'],
        defaultValue: DefaultSettings.sumTimings,
      })

      .addBooleanSwitch({
        path: 'filterEmptyConnections',
        name: 'Filter Empty Data',
        description:
          'If this setting is active, the timings provided by ' +
          'the mapped response time columns are considered as a continually ' +
          'increasing sum of response times. When deactivated, it is considered ' +
          'that the timings provided by columns are the actual average response times.',
        category: ['General Settings'],
        defaultValue: DefaultSettings.filterEmptyConnections,
      })

      .addBooleanSwitch({
        path: 'showDebugInformation',
        name: 'Show Debug Information',
        category: ['General Settings'],
        defaultValue: DefaultSettings.showDebugInformation,
      })

      .addCustomEditor({
        path: 'dataMapping',
        id: 'dummyDataSwitch',
        name: 'Show Dummy Data',
        editor: DummyDataSwitch,
        category: ['General Settings'],
        defaultValue: DefaultSettings.dataMapping,
      })

      .addBooleanSwitch({
        path: 'showBaselines',
        name: 'Show Baselines',
        category: ['General Settings'],
        defaultValue: DefaultSettings.showBaselines,
      })

      .addSelect({
        path: 'timeFormat',
        name: 'Maximum Time Unit to Resolve',
        description:
          'This setting controls to which time unit time values will be resolved to. ' +
          'Each value always includes the smaller units.',
        category: ['General Settings'],
        settings: {
          options: [
            { value: 'ms', label: 'ms' },
            { value: 's', label: 's' },
            { value: 'm', label: 'm' },
          ],
        },
        defaultValue: DefaultSettings.timeFormat,
      })

      //Appearance
      .addColorPicker({
        path: 'style.healthyColor',
        name: 'Healthy Color',
        category: ['Appearance'],
        defaultValue: DefaultSettings.style.healthyColor,
      })

      .addColorPicker({
        path: 'style.dangerColor',
        name: 'Danger Color',
        category: ['Appearance'],
        defaultValue: DefaultSettings.style.dangerColor,
      })

      .addColorPicker({
        path: 'style.noDataColor',
        name: 'No Data Color',
        category: ['Appearance'],
        defaultValue: DefaultSettings.style.noDataColor,
      })

      //Icon Mapping
      .addCustomEditor({
        path: 'icons',
        id: 'iconMapping',
        editor: IconMapping,
        name: '',
        description:
          'This setting controls which images should be mapped to your directly monitored nodes. ' +
          'The node names are matched by the regex pattern provided in the "Target Name(Regex) column.',
        category: ['Icon Mapping'],
        defaultValue: DefaultSettings.icons,
      })

      //External Icon Mapping
      .addCustomEditor({
        path: 'externalIcons',
        id: 'externalIconMapping',
        editor: IconMapping,
        name: '',
        description:
          'This setting controls which images should be mapped to the external nodes. ' +
          'The given type column is matched by the regex pattern provided in the "Target Name(Regex) column.',
        category: ['External Icon Mapping'],
        defaultValue: DefaultSettings.externalIcons,
      })

      //Tracing Drilldown
      .addTextInput({
        path: 'drillDownLink',
        name: 'Backend URL',
        category: ['Tracing Drilldown'],
        defaultValue: DefaultSettings.drillDownLink,
      })
  );
};


================================================
FILE: src/panel/PanelController.tsx
================================================
import React, { LegacyRef, PureComponent } from 'react';
import {
  AbsoluteTimeRange,
  DataFrame,
  FieldConfigSource,
  InterpolateFunction,
  PanelProps,
  TimeRange,
} from '@grafana/data';
import { ServiceDependencyGraph } from './serviceDependencyGraph/ServiceDependencyGraph';
import _ from 'lodash';
import { CurrentData, CyData, IntGraph, IntGraphEdge, IntGraphNode, PanelSettings } from '../types';
import cytoscape, { EdgeSingular, NodeSingular } from 'cytoscape';
import '../css/novatec-service-dependency-graph-panel.css';
import GraphGenerator from 'processing/graph_generator';
import PreProcessor from 'processing/pre_processor';
import data from '../dummy_data_frame';
import { getTemplateSrv } from '@grafana/runtime';

interface Props extends PanelProps<PanelSettings> {}

interface PanelState {
  id: string | number;
  fieldConfig: FieldConfigSource<any>;
  height: number;
  width: number;
  onChangeTimeRange: (timeRange: AbsoluteTimeRange) => void;
  onFieldConfigChange: (config: FieldConfigSource<any>) => void;
  onOptionsChange: (options: PanelSettings) => void;
  renderCounter: number;
  replaceVariables: InterpolateFunction;
  timeRange: TimeRange;
  timeZone: string;
  title: string;
  transparent: boolean;
  options: PanelSettings;
  currentLayer: number;
}

export class PanelController extends PureComponent<Props, PanelState> {
  cy: cytoscape.Core | undefined;

  ref: LegacyRef<HTMLDivElement>;

  validQueryTypes: boolean;

  graphGenerator: GraphGenerator;

  preProcessor: PreProcessor;

  currentData: CurrentData;

  maxLayer = 0;

  constructor(props: Props) {
    super(props);
    this.state = {
      currentLayer: 0,
      ...props,
    };
    this.ref = React.createRef();
    this.graphGenerator = new GraphGenerator(this);
    this.preProcessor = new PreProcessor(this);
  }

  getSettings(resolveVariables: boolean): PanelSettings {
    if (resolveVariables) {
      return this.resolveVariables(this.props.options);
    }
    return this.props.options;
  }

  resolveVariables(element: any) {
    if (element instanceof Object) {
      const newObject: any = {};
      for (const key of Object.keys(element)) {
        newObject[key] = this.resolveVariables(element[key]);
      }
      return newObject;
    }

    if (element instanceof String || typeof element === 'string') {
      return getTemplateSrv().replace(element.toString());
    }
    return element;
  }

  resolveTemplateVars(input: any, copy: boolean) {
    let value = input;
    if (copy) {
      value = _.cloneDeep(value);
    }

    if (typeof value === 'string' || value instanceof String) {
      value = getTemplateSrv().replace(value.toString());
    }
    if (value instanceof Object) {
      for (const key of Object.keys(value)) {
        value[key] = this.resolveTemplateVars(value[key], false);
      }
    }
    return value;
  }

  componentDidUpdate() {
    this.processData();
  }

  processQueryData(data: DataFrame[]) {
    this.validQueryTypes = this.hasOnlyTableQueries(data);
    const graphData = this.preProcessor.processData(data);

    this.currentData = graphData;
  }

  hasOnlyTableQueries(inputData: DataFrame[]) {
    let result = true;

    _.each(inputData, (dataElement) => {
      if (!_.has(dataElement, 'columns')) {
        result = false;
      }
    });

    return result;
  }

  processData() {
    let inputData: DataFrame[] = this.props.data.series;
    if (this.getSettings(true).dataMapping.showDummyData) {
      inputData = data;
    }
    this.processQueryData(inputData);
    const graph: IntGraph = this.graphGenerator.generateGraph(this.currentData.graph);
    return graph;
  }

  _transformEdges(edges: IntGraphEdge[]): CyData[] {
    const cyEdges = _.map(edges, (edge) => {
      const cyEdge = {
        group: 'edges',
        data: {
          id: edge.source + ':' + edge.target,
          source: edge.source,
          target: edge.target,
          metrics: {
            ...edge.metrics,
          },
        },
      };
      return cyEdge;
    });

    return cyEdges;
  }

  _transformNodes(nodes: IntGraphNode[]): CyData[] {
    const cyNodes = _.map(nodes, (node) => {
      const result: CyData = {
        group: 'nodes',
        data: {
          id: node.data.id,
          type: node.data.type,
          external_type: node.data.external_type,
          namespace: node.data.namespace,
          layer: node.data.layer,
          parent: node.data.parent,
          metrics: {
            ...node.data.metrics,
          },
        },
      };
      return result;
    });

    return cyNodes;
  }

  _updateOrRemove(dataArray: Array<NodeSingular | EdgeSingular>, inputArray: CyData[]) {
    const elements: Array<NodeSingular | EdgeSingular> = [];
    for (let i = 0; i < dataArray.length; i++) {
      const element = dataArray[i];

      const cyNode = _.find(inputArray, { data: { id: element.id() } });

      if (cyNode) {
        element.data(cyNode.data);
        _.remove(inputArray, (n) => n.data.id === cyNode.data.id);
        elements.push(element);
      } else {
        element.remove();
      }
    }
    return elements;
  }

  getError(): string | null {
    if (!this.isDataAvailable()) {
      return 'No data to show - the query returned no data.';
    }
    return null;
  }

  isDataAvailable() {
    const dataExist =
      !_.isUndefined(this.currentData) && !_.isUndefined(this.currentData.graph) && this.currentData.graph.length > 0;
    return dataExist;
  }

  layer(layerIncrease: number) {
    const that = this;
    const currentLayer = that.state ? that.state.currentLayer : 0;
    let layer = Math.max(0, currentLayer + layerIncrease);
    if (layerIncrease > 0) {
      layer = Math.min(that.maxLayer, currentLayer + layerIncrease);
    }
    that.setState({
      currentLayer: layer,
    });
  }

  render() {
    const data = this.processData();
    const error = this.getError();
    if (error === null) {
      // This is our root DOM
      return (
        <div>
          <div
            // The className is also used to scope all of our css rules
            className="service-dependency-graph-panel"
            style={{ height: this.props.height, width: this.props.width }}
            ref={this.ref}
            id="cy"
          >
            <ServiceDependencyGraph
              data={data}
              zoom={1}
              maxLayer={this.maxLayer}
              controller={this}
              animate={false}
              showStatistics={false}
              settings={this.props.options}
              layerIncreaseFunction={() => this.layer(+1)}
              layerDecreaseFunction={() => this.layer(-1)}
              layer={0}
            />
          </div>
        </div>
      );
    } else {
      return <div>{error}</div>;
    }
  }
}


================================================
FILE: src/panel/asset_utils.tsx
================================================
import { find } from 'lodash';
import { IconResource } from 'types';

export default {
  getAssetUrl(assetName: string) {
    let baseUrl = 'public/plugins/novatec-sdg-panel';
    return baseUrl + '/assets/icons/' + assetName;
  },

  getTypeSymbol(type: string, externalIcons: IconResource[], resolveName = true) {
    if (!type) {
      return this.getAssetUrl('default.png');
    }

    if (!resolveName) {
      return this.getAssetUrl(type);
    }

    const icon = find(externalIcons, (icon) => icon.pattern.toLowerCase() === type.toLowerCase());

    if (icon !== undefined) {
      return this.getAssetUrl(icon.filename + '.png');
    } else {
      return this.getAssetUrl('default.png');
    }
  },
};


================================================
FILE: src/panel/canvas/collision_detector.ts
================================================
import _ from 'lodash';
import { Point, Rectangle } from 'types';

export default class CollisionDetector {
  blockedArea: Rectangle[];

  constructor() {
    this.blockedArea = [];
  }

  reset() {
    this.blockedArea = [];
  }

  addRectangle(x: number, y: number, width: number, height: number) {
    const rectangle: Rectangle = {
      coordinates: {
        x: x,
        y: y,
      },
      height: height,
      width: width,
    };
    this.blockedArea.push(rectangle);
  }

  isColliding(shape: Rectangle) {
    const collidingShape = this.blockedArea.find((blockingShape) => {
      if (this._intersects(shape, blockingShape)) {
        return true;
      }
      return false;
    });
    return collidingShape !== undefined;
  }

  _intersects(a: Rectangle, b: Rectangle) {
    const topLeft1: Point = a.coordinates;
    const topLeft2: Point = b.coordinates;
    const bottomRight1 = this._getBottomRightCorner(a);
    const bottomRight2 = this._getBottomRightCorner(b);

    if (topLeft1.x > bottomRight2.x || topLeft2.x > bottomRight1.x) {
      return false;
    }
    if (topLeft1.y > bottomRight2.y || topLeft2.y > bottomRight1.y) {
      return false;
    }
    return true;
  }

  _getBottomRightCorner(rectangle: Rectangle) {
    const cornerPoint: Point = {
      x: rectangle.coordinates.x + rectangle.width,
      y: rectangle.coordinates.y + rectangle.height,
    };
    return cornerPoint;
  }
}


================================================
FILE: src/panel/canvas/graph_canvas.ts
================================================
import _ from 'lodash';
import cytoscape from 'cytoscape';
import { ServiceDependencyGraph } from '../serviceDependencyGraph/ServiceDependencyGraph';
import ParticleEngine from './particle_engine';
import {
  CyCanvas,
  Particle,
  EnGraphNodeType,
  Particles,
  IntGraphMetrics,
  ScaleValue,
  DrawContext,
  Rectangle,
  Point,
} from '../../types';
import humanFormat from 'human-format';
import assetUtils from '../asset_utils';
import CollisionDetector from './collision_detector';

const scaleValues: ScaleValue[] = [
  { unit: 'ms', factor: 1 },
  { unit: 's', factor: 1000 },
  { unit: 'm', factor: 60000 },
];

export default class CanvasDrawer {
  readonly colors = {
    default: '#bad5ed',
    background: '#212121',
    edge: '#505050',
    status: {
      warning: 'orange',
    },
  };

  readonly donutRadius: number = 15;

  controller: ServiceDependencyGraph;

  cytoscape: cytoscape.Core;

  context: CanvasRenderingContext2D;

  cyCanvas: CyCanvas;

  canvas: HTMLCanvasElement;

  offscreenCanvas: HTMLCanvasElement;

  offscreenContext: CanvasRenderingContext2D;

  frameCounter = 0;

  fpsCounter = 0;

  particleImage: HTMLImageElement;

  pixelRatio: number;

  imageAssets: any = {};

  selectionNeighborhood: cytoscape.Collection;

  particleEngine: ParticleEngine;

  collisionDetector: CollisionDetector;

  lastRenderTime = 0;

  dashAnimationOffset = 0;

  constructor(ctrl: ServiceDependencyGraph, cy: cytoscape.Core, cyCanvas: CyCanvas) {
    this.cytoscape = cy;
    this.cyCanvas = cyCanvas;
    this.controller = ctrl;
    this.particleEngine = new ParticleEngine(this);
    this.collisionDetector = new CollisionDetector();

    this.pixelRatio = window.devicePixelRatio || 1;

    this.canvas = cyCanvas.getCanvas();
    const ctx = this.canvas.getContext('2d');
    if (ctx) {
      this.context = ctx;
    } else {
      console.error('Could not get 2d canvas context.');
    }

    this.offscreenCanvas = document.createElement('canvas');
    this.offscreenContext = this.offscreenCanvas.getContext('2d');

    this.repaint(true);
  }

  _getTimeScale(timeUnit: string) {
    const scale: any = {};
    for (const scaleValue of scaleValues) {
      scale[scaleValue.unit] = scaleValue.factor;
      if (scaleValue.unit === timeUnit) {
        return scale;
      }
    }
    return scale;
  }

  resetAssets() {
    this.imageAssets = {};
  }

  _loadImage(imageUrl: string, assetName: string) {
    const that = this;

    const loadImage = (url: string, asset: keyof typeof that.imageAssets) => {
      const image = new Image();
      that.imageAssets[asset] = {
        image,
        loaded: false,
      };

      return new Promise((resolve, reject) => {
        image.onload = () => resolve(asset);
        image.onerror = () => reject(new Error(`load ${url} fail`));
        image.src = url;
      });
    };
    loadImage(imageUrl, assetName).then((asset: any) => {
      that.imageAssets[asset].loaded = true;
    });
  }

  _isImageLoaded(assetName: string) {
    if (_.has(this.imageAssets, assetName) && this.imageAssets[assetName].loaded) {
      return true;
    } else {
      return false;
    }
  }

  _getImageAsset(assetName: string, resolveName = true) {
    if (!_.has(this.imageAssets, assetName)) {
      const { externalIcons } = this.controller.getSettings(true);
      const assetUrl = assetUtils.getTypeSymbol(assetName, externalIcons, resolveName);
      this._loadImage(assetUrl, assetName);
    }

    if (this._isImageLoaded(assetName)) {
      return this.imageAssets[assetName].image;
    } else {
      return null;
    }
  }

  _getAsset(assetName: string, relativeUrl: string) {
    if (!_.has(this.imageAssets, assetName)) {
      const assetUrl = assetUtils.getAssetUrl(relativeUrl);
      this._loadImage(assetUrl, assetName);
    }

    if (this._isImageLoaded(assetName)) {
      return this.imageAssets[assetName].image;
    } else {
      return null;
    }
  }

  start() {
    const that = this;
    const repaintWrapper = () => {
      that.repaint();
      window.requestAnimationFrame(repaintWrapper);
    };

    window.requestAnimationFrame(repaintWrapper);

    setInterval(() => {
      that.fpsCounter = that.frameCounter;
      that.frameCounter = 0;
    }, 1000);
  }

  startAnimation() {
    this.particleEngine.start();
  }

  stopAnimation() {
    this.particleEngine.stop();
    this.repaint();
  }

  _skipFrame() {
    const now = Date.now();
    const elapsedTime = now - this.lastRenderTime;

    if (this.particleEngine.count() > 0) {
      return false;
    }

    if (!this.controller.getSettings(true).animate && elapsedTime < 1000) {
      return true;
    }
    return false;
  }

  repaint(forceRepaint = false) {
    if (!forceRepaint && this._skipFrame()) {
      return;
    }
    this.lastRenderTime = Date.now();

    const ctx = this.context;
    const cyCanvas = this.cyCanvas;
    const offscreenCanvas = this.offscreenCanvas;
    const offscreenContext = this.offscreenContext;
    this.collisionDetector.reset();

    offscreenCanvas.width = this.canvas.width;
    offscreenCanvas.height = this.canvas.height;

    // offscreen rendering
    this._setTransformation(offscreenContext);

    this.selectionNeighborhood = this.cytoscape.collection();
    const selection = this.cytoscape.$(':selected');
    selection.forEach((element: cytoscape.SingularElementArgument) => {
      this.selectionNeighborhood.merge(element);

      if (element.isNode()) {
        const neighborhood = element.neighborhood();
        this.selectionNeighborhood.merge(neighborhood);
      } else {
        const source = element.source();
        const target = element.target();
        this.selectionNeighborhood.merge(source);
        this.selectionNeighborhood.merge(target);
      }
    });

    this._drawEdgeAnimation(offscreenContext);
    this._drawNodes(offscreenContext);

    // static element rendering
    // cyCanvas.resetTransform(ctx);
    cyCanvas.clear(ctx);
    if (this.controller.getSettings(true).showDebugInformation) {
      this._drawDebugInformation();
    }

    if (offscreenCanvas.width > 0 && offscreenCanvas.height > 0) {
      ctx.drawImage(offscreenCanvas, 0, 0);
    }

    // baseline animation
    this.dashAnimationOffset = (Date.now() % 60000) / 250;
  }

  _setTransformation(ctx: CanvasRenderingContext2D) {
    const pan = this.cytoscape.pan();
    const zoom = this.cytoscape.zoom();
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.translate(pan.x * this.pixelRatio, pan.y * this.pixelRatio);
    ctx.scale(zoom * this.pixelRatio, zoom * this.pixelRatio);
  }

  _drawEdgeAnimation(ctx: CanvasRenderingContext2D) {
    const now = Date.now();

    ctx.save();

    const edges = this.cytoscape.edges().toArray();
    const hasSelection = this.selectionNeighborhood.size() > 0;

    const transparentEdges = edges.filter((edge) => hasSelection && !this.selectionNeighborhood.has(edge));
    const opaqueEdges = edges.filter((edge) => !hasSelection || this.selectionNeighborhood.has(edge));

    ctx.globalAlpha = 0.25;
    this._drawEdges(ctx, transparentEdges, now);
    ctx.globalAlpha = 1;
    this._drawEdges(ctx, opaqueEdges, now);
    ctx.restore();
  }

  _drawEdges(ctx: CanvasRenderingContext2D, edges: cytoscape.EdgeSingular[], now: number) {
    const cy = this.cytoscape;

    for (const edge of edges) {
      const sourcePoint = edge.sourceEndpoint();
      const targetPoint = edge.targetEndpoint();
      this._drawEdgeLine(ctx, edge, sourcePoint, targetPoint);
      this._drawEdgeParticles(ctx, edge, sourcePoint, targetPoint, now);
    }

    const { showConnectionStats } = this.controller.getSettings(true);
    if (showConnectionStats && cy.zoom() > 1) {
      for (const edge of edges) {
        this._drawEdgeLabel(ctx, edge);
      }
    }
  }

  _drawEdgeLine(
    ctx: CanvasRenderingContext2D,
    edge: cytoscape.EdgeSingular,
    sourcePoint: cytoscape.Position,
    targetPoint: cytoscape.Position
  ) {
    ctx.beginPath();

    ctx.moveTo(sourcePoint.x, sourcePoint.y);
    ctx.lineTo(targetPoint.x, targetPoint.y);

    const metrics = edge.data('metrics');
    const requestCount = _.get(metrics, 'normal', -1);
    const errorCount = _.get(metrics, 'danger', -1);

    let base;
    if (!this.selectionNeighborhood.empty() && this.selectionNeighborhood.has(edge)) {
      ctx.lineWidth = 3;
      base = 140;
    } else {
      ctx.lineWidth = 1;
      base = 80;
    }

    if (requestCount >= 0 && errorCount >= 0) {
      const range = 255;

      const factor = errorCount / requestCount;
      const color = Math.min(255, base + range * Math.log2(factor + 1));

      ctx.strokeStyle = 'rgb(' + color + ',' + base + ',' + base + ')';
    } else {
      ctx.strokeStyle = 'rgb(' + base + ',' + base + ',' + base + ')';
    }

    ctx.stroke();
  }

  _drawEdgeLabel(ctx: CanvasRenderingContext2D, edge: cytoscape.EdgeSingular) {
    const { timeFormat } = this.controller.getSettings(true);

    const midpoint = edge.midpoint();
    const xMid = midpoint.x;
    const yMid = midpoint.y;

    let statistics: string[] = [];
    const metrics: IntGraphMetrics = edge.data('metrics');
    const duration = _.defaultTo(metrics.response_time, -1);
    const requestCount = _.defaultTo(metrics.rate, -1);
    const errorCount = _.defaultTo(metrics.error_rate, -1);

    const timeScale = new humanFormat.Scale(this._getTimeScale(timeFormat));

    if (duration >= 0) {
      const decimals = duration >= 1000 ? 1 : 0;
      statistics.push(humanFormat(duration, { scale: timeScale, decimals }));
    }
    if (requestCount >= 0) {
      const decimals = requestCount >= 1000 ? 1 : 0;
      statistics.push(humanFormat(parseFloat(requestCount.toString()), { decimals }) + ' Req.');
    }
    if (errorCount >= 0) {
      const decimals = errorCount >= 1000 ? 1 : 0;
      statistics.push(humanFormat(errorCount, { decimals }) + ' Err.');
    }

    if (statistics.length > 0) {
      const edgeLabel = statistics.join(', ');
      this._drawLabel(ctx, edgeLabel, xMid, yMid, edge);
    }
  }

  _drawEdgeParticles(
    ctx: CanvasRenderingContext2D,
    edge: cytoscape.EdgeSingular,
    sourcePoint: cytoscape.Position,
    targetPoint: cytoscape.Position,
    now: number
  ) {
    const particles: Particles = edge.data('particles');

    if (particles === undefined) {
      return;
    }

    const xVector = targetPoint.x - sourcePoint.x;
    const yVector = targetPoint.y - sourcePoint.y;

    const angle = Math.atan2(yVector, xVector);
    const xDirection = Math.cos(angle);
    const yDirection = Math.sin(angle);

    const xMinLimit = Math.min(sourcePoint.x, targetPoint.x);
    const xMaxLimit = Math.max(sourcePoint.x, targetPoint.x);
    const yMinLimit = Math.min(sourcePoint.y, targetPoint.y);
    const yMaxLimit = Math.max(sourcePoint.y, targetPoint.y);

    const drawContext: DrawContext = {
      ctx,
      now,
      xDirection,
      yDirection,
      xMinLimit,
      xMaxLimit,
      yMinLimit,
      yMaxLimit,
      sourcePoint,
    };

    // normal particles
    ctx.beginPath();

    let index = particles.normal.length - 1;
    while (index >= 0) {
      this._drawParticle(drawContext, particles.normal, index);
      index--;
    }

    ctx.fillStyle = '#d1e2f2';
    ctx.fill();

    // danger particles
    ctx.beginPath();

    index = particles.danger.length - 1;
    while (index >= 0) {
      this._drawParticle(drawContext, particles.danger, index);
      index--;
    }

    const dangerColor = this.controller.getSettings(true).style.dangerColor;
    ctx.fillStyle = dangerColor;
    ctx.fill();
  }

  _drawLabel(ctx: CanvasRenderingContext2D, label: string, cX: number, cY: number, edge: cytoscape.EdgeSingular) {
    const labelPadding = 1;
    ctx.font = '6px Arial';

    const labelWidth = ctx.measureText(label).width;
    let xPos = cX - labelWidth / 2;
    let yPos = cY + 3;
    let labelArea: Rectangle = {
      coordinates: {
        x: xPos - labelPadding + 4,
        y: yPos - 6 - labelPadding + 4,
      },
      width: labelWidth,
      height: 6,
    };

    if (!isNaN(labelArea.coordinates.x) || !isNaN(labelArea.coordinates.y) || !isNaN(xPos) || !isNaN(yPos)) {
      // TODO: Rather than using a fixed number here, we should find a way to compute a second boundary condition smarter.
      // This is for the case when all nodes are so close together the labels need to overlap each other.
      const maxRepeats = 1000;
      let repeats = 0;
      while (this.collisionDetector.isColliding(labelArea) && repeats < maxRepeats) {
        const nextPoint = this._getNextPointOnVector(xPos, yPos, edge, 0.999);
        labelArea.coordinates = nextPoint;
        yPos = nextPoint.y;
        xPos = nextPoint.x;
        repeats++;
      }
      this.collisionDetector.addRectangle(xPos - 4, yPos - 4, labelWidth + 12, 12);
    }

    ctx.fillStyle = this.colors.default;
    ctx.fillRect(xPos - labelPadding, yPos - 6 - labelPadding, labelWidth + 2 * labelPadding, 6 + 2 * labelPadding);
    ctx.fillStyle = this.colors.background;
    ctx.fillText(label, xPos, yPos);
  }

  _getNextPointOnVector(x: number, y: number, edge: cytoscape.EdgeSingular, step: number) {
    let yTarget = edge.sourceEndpoint().y;
    let xTarget = edge.sourceEndpoint().x;

    const newPoint: Point = {
      x: xTarget * (1.0 - step) + x * step,
      y: yTarget * (1.0 - step) + y * step,
    };

    return newPoint;
  }

  _drawParticle(drawCtx: DrawContext, particles: Particle[], index: number) {
    const { ctx, now, xDirection, yDirection, xMinLimit, xMaxLimit, yMinLimit, yMaxLimit, sourcePoint } = drawCtx;

    const particle = particles[index];

    const timeDelta = now - particle.startTime;
    const xPos = sourcePoint.x + xDirection * timeDelta * particle.velocity;
    const yPos = sourcePoint.y + yDirection * timeDelta * particle.velocity;

    if (xPos > xMaxLimit || xPos < xMinLimit || yPos > yMaxLimit || yPos < yMinLimit) {
      // remove particle
      particles.splice(index, 1);
    } else {
      // draw particle
      ctx.moveTo(xPos, yPos);
      ctx.arc(xPos, yPos, 1, 0, 2 * Math.PI, false);
    }
  }

  _drawNodes(ctx: CanvasRenderingContext2D) {
    const that = this;
    const cy = this.cytoscape;

    // Draw model elements
    const nodes = cy.nodes().toArray();
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if (that.selectionNeighborhood.empty() || that.selectionNeighborhood.has(node)) {
        ctx.globalAlpha = 1;
      } else {
        ctx.globalAlpha = 0.25;
      }

      // draw the node
      if (node.data().type === 'PARENT') {
        if (
          node.data().layer >= this.controller.state.controller.state.currentLayer ||
          node.data().layer === undefined
        ) {
          that._drawNode(ctx, node);
        }
      } else {
        that._drawNode(ctx, node);
      }

      // drawing the node label in case we are not zoomed out
      if (cy.zoom() > 1) {
        that._drawNodeLabel(ctx, node);
      }
    }
  }

  _drawNode(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSingular) {
    const cy = this.cytoscape;
    const type = node.data('type');
    const metrics: IntGraphMetrics = node.data('metrics');

    if (type === EnGraphNodeType.INTERNAL) {
      const requestCount = _.defaultTo(metrics.rate, -1);
      const errorCount = _.defaultTo(metrics.error_rate, 0);
      const responseTime = _.defaultTo(metrics.response_time, -1);
      const threshold = _.defaultTo(metrics.threshold, -1);

      let unknownPct;
      let errorPct;
      let healthyPct;
      if (requestCount < 0) {
        healthyPct = 0;
        errorPct = 0;
        unknownPct = 1;
      } else {
        if (errorCount <= 0) {
          errorPct = 0.0;
        } else {
          errorPct = (1.0 / requestCount) * errorCount;
        }
        healthyPct = 1.0 - errorPct;
        unknownPct = 0;
      }

      // drawing the donut
      this._drawDonut(ctx, node, 15, 5, 0.5, [errorPct, unknownPct, healthyPct]);

      // drawing the baseline status
      const { showBaselines } = this.controller.getSettings(true);
      if (showBaselines && responseTime >= 0 && threshold >= 0) {
        const thresholdViolation = threshold < responseTime;

        this._drawThresholdStroke(ctx, node, thresholdViolation, 15, 5, 0.5);
      }
      this._drawServiceIcon(ctx, node);
    } else {
      this._drawExternalService(ctx, node);
    }

    // draw statistics
    if (cy.zoom() > 1) {
      this._drawNodeStatistics(ctx, node);
    }
  }

  _drawServiceIcon(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSingular) {
    const nodeId: string = node.id();
    const iconMappings = this.controller.getSettings(true).icons;

    const mapping = _.find(iconMappings, ({ pattern }) => {
      try {
        return new RegExp(pattern).test(nodeId);
      } catch (error) {
        return false;
      }
    });

    if (mapping) {
      const image = this._getAsset(mapping.filename, mapping.filename + '.png');
      if (image != null) {
        const cX = node.position().x;
        const cY = node.position().y;
        const iconSize = 16;

        ctx.drawImage(image, cX - iconSize / 2, cY - iconSize / 2, iconSize, iconSize);
      }
    }
  }

  _drawNodeStatistics(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSingular) {
    const { timeFormat } = this.controller.getSettings(true);
    const lines: string[] = [];

    const metrics: IntGraphMetrics = node.data('metrics');
    const requestCount = _.defaultTo(metrics.rate, -1);
    const errorCount = _.defaultTo(metrics.error_rate, -1);
    const responseTime = _.defaultTo(metrics.response_time, -1);

    const timeScale = new humanFormat.Scale(this._getTimeScale(timeFormat));

    if (requestCount >= 0) {
      const decimals = requestCount >= 1000 ? 1 : 0;
      lines.push('Requests: ' + humanFormat(parseFloat(requestCount.toString()), { decimals }));
    }
    if (errorCount >= 0) {
      const decimals = errorCount >= 1000 ? 1 : 0;
      lines.push('Errors: ' + humanFormat(errorCount, { decimals }));
    }
    if (responseTime >= 0) {
      const decimals = responseTime >= 1000 ? 1 : 0;

      lines.push('Avg. Resp. Time: ' + humanFormat(responseTime, { scale: timeScale, decimals }));
    }

    const pos = node.position();
    const fontSize = 6;
    const cX = pos.x + this.donutRadius * 1.25;
    const cY = pos.y + fontSize / 2 - (fontSize / 2) * (lines.length - 1);

    ctx.font = '6px Arial';
    ctx.fillStyle = this.colors.default;
    for (let i = 0; i < lines.length; i++) {
      ctx.fillText(lines[i], cX, cY + i * fontSize);
    }
  }

  _drawThresholdStroke(
    ctx: CanvasRenderingContext2D,
    node: cytoscape.NodeSingular,
    violation: boolean,
    radius: number,
    width: number,
    baseStrokeWidth: number
  ) {
    const pos = node.position();
    const cX = pos.x;
    const cY = pos.y;

    const strokeWidth = baseStrokeWidth * 2 * (violation ? 1.5 : 1);
    const offset = strokeWidth * 0.2;

    ctx.beginPath();
    ctx.arc(cX, cY, radius + strokeWidth - offset, 0, 2 * Math.PI, false);
    ctx.closePath();
    ctx.setLineDash([]);
    ctx.lineWidth = strokeWidth * 1;
    ctx.strokeStyle = 'white';
    ctx.stroke();

    ctx.beginPath();
    ctx.arc(cX, cY, radius + strokeWidth - offset, 0, 2 * Math.PI, false);
    ctx.closePath();

    ctx.setLineDash([10, 2]);
    if (violation && this.controller.getSettings(true).animate) {
      ctx.lineDashOffset = this.dashAnimationOffset;
    } else {
      ctx.lineDashOffset = 0;
    }
    ctx.lineWidth = strokeWidth;
    ctx.strokeStyle = violation ? 'rgb(184, 36, 36)' : '#37872d';

    ctx.stroke();

    // inner
    ctx.beginPath();
    ctx.arc(cX, cY, radius - width - baseStrokeWidth, 0, 2 * Math.PI, false);
    ctx.closePath();
    ctx.fillStyle = violation ? 'rgb(184, 36, 36)' : '#37872d';
    ctx.fill();
  }

  _drawExternalService(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSingular) {
    const pos = node.position();
    const cX = pos.x;
    const cY = pos.y;
    const size = 12;

    ctx.beginPath();
    ctx.arc(cX, cY, 12, 0, 2 * Math.PI, false);
    ctx.fillStyle = 'white';
    ctx.fill();

    ctx.beginPath();
    ctx.arc(cX, cY, 11.5, 0, 2 * Math.PI, false);
    ctx.fillStyle = this.colors.background;
    ctx.fill();

    const nodeType = node.data('external_type');

    const image = this._getImageAsset(nodeType);
    if (image != null) {
      ctx.drawImage(image, cX - size / 2, cY - size / 2, size, size);
    }
  }

  _drawNodeLabel(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSingular) {
    const pos = node.position();
    let label: string = node.id();
    const labelPadding = 1;

    if (this.selectionNeighborhood.empty() || !this.selectionNeighborhood.has(node)) {
      if (label.length > 20) {
        label = label.substr(0, 7) + '...' + label.slice(-7);
      }
    }

    ctx.font = '6px Arial';

    const labelWidth = ctx.measureText(label).width;
    const xPos = pos.x - labelWidth / 2;
    let yPos = pos.y + node.height() * 0.8;

    if (node.data().type === 'PARENT') {
      if (node.data().layer >= this.controller.state.controller.state.currentLayer || node.data().layer === undefined) {
      } else {
        yPos = pos.y + node.height() * 0.5 + 30;
      }
    }

    const { showBaselines } = this.controller.getSettings(true);
    const metrics: IntGraphMetrics = node.data('metrics');
    const responseTime = _.defaultTo(metrics.response_time, -1);
    const threshold = _.defaultTo(metrics.threshold, -1);

    if (!showBaselines || threshold < 0 || responseTime < 0 || responseTime <= threshold) {
      ctx.fillStyle = this.colors.default;
    } else {
      ctx.fillStyle = '#FF7383';
    }

    ctx.fillRect(xPos - labelPadding, yPos - 6 - labelPadding, labelWidth + 2 * labelPadding, 6 + 2 * labelPadding);

    ctx.fillStyle = this.colors.background;
    ctx.fillText(label, xPos, yPos);
  }

  _drawDebugInformation() {
    const ctx = this.context;

    this.frameCounter++;

    ctx.font = '12px monospace';
    ctx.fillStyle = 'white';
    ctx.fillText('Frames per Second: ' + this.fpsCounter, 10, 12);
    ctx.fillText('Particles: ' + this.particleEngine.count(), 10, 24);
  }

  _drawDonut(
    ctx: CanvasRenderingContext2D,
    node: cytoscape.NodeSingular,
    radius: number,
    width: number,
    strokeWidth: number,
    percentages: number[]
  ) {
    const cX = node.position().x;
    const cY = node.position().y;
    let currentArc = -Math.PI / 2; // offset

    ctx.beginPath();
    ctx.arc(cX, cY, radius + strokeWidth, 0, 2 * Math.PI, false);
    ctx.closePath();
    ctx.fillStyle = 'white';
    ctx.fill();

    const { healthyColor, dangerColor, noDataColor } = this.controller.getSettings(true).style;
    const colors = [dangerColor, noDataColor, healthyColor];
    for (let i = 0; i < percentages.length; i++) {
      let arc = this._drawArc(ctx, currentArc, cX, cY, radius, percentages[i], colors[i]);
      currentArc += arc;
    }

    ctx.beginPath();
    ctx.arc(cX, cY, radius - width, 0, 2 * Math.PI, false);
    ctx.fillStyle = 'white';
    ctx.fill();

    // cut out an inner-circle == donut
    ctx.beginPath();
    ctx.arc(cX, cY, radius - width - strokeWidth, 0, 2 * Math.PI, false);
    if (node.selected()) {
      ctx.fillStyle = 'white';
    } else {
      ctx.fillStyle = this.colors.background;
    }
    ctx.fill();
  }

  _drawArc(
    ctx: CanvasRenderingContext2D,
    currentArc: number,
    cX: number,
    cY: number,
    radius: number,
    percent: number,
    color: string
  ) {
    // calc size of our wedge in radians
    let WedgeInRadians = (percent * 360 * Math.PI) / 180;
    // draw the wedge
    ctx.save();
    ctx.beginPath();
    ctx.moveTo(cX, cY);
    ctx.arc(cX, cY, radius, currentArc, currentArc + WedgeInRadians, false);
    ctx.closePath();
    ctx.fillStyle = color;
    ctx.fill();
    ctx.restore();
    // sum the size of all wedges so far
    // We will begin our next wedge at this sum
    return WedgeInRadians;
  }
}


================================================
FILE: src/panel/canvas/particle_engine.ts
================================================
import CanvasDrawer from './graph_canvas';
import _ from 'lodash';
import { Particles, Particle, IntGraphMetrics } from '../../types';

export default class ParticleEngine {
  drawer: CanvasDrawer;

  maxVolume = 800;

  minSpawnPropability = 0.004;

  spawnInterval: NodeJS.Timeout;

  animating: boolean;

  constructor(canvasDrawer: CanvasDrawer) {
    this.drawer = canvasDrawer;
    this.animating = false;
  }

  start() {
    this.animating = true;
    if (!this.spawnInterval) {
      const that = this;
      this.spawnInterval = setInterval(() => that.animate(), 60);
    }
  }

  stop() {
    this.animating = false;
  }

  animate() {
    const that = this;
    if (!that.animating) {
      if (!this.hasParticles()) {
        clearInterval(this.spawnInterval);
        this.spawnInterval = null;
      }
    } else {
      that._spawnParticles();
    }
    that.drawer.repaint();
  }

  hasParticles() {
    for (const edge of this.drawer.cytoscape.edges().toArray()) {
      if (
        edge.data('particles') !== undefined &&
        (edge.data('particles').normal.length > 0 || edge.data('particles').danger.length > 0)
      ) {
        return true;
      }
    }
    return false;
  }

  _spawnParticles() {
    const cy = this.drawer.cytoscape;

    const now = Date.now();
    cy.edges().forEach((edge) => {
      let particles: Particles = edge.data('particles');
      const metrics: IntGraphMetrics = edge.data('metrics');

      if (!metrics) {
        return;
      }

      const rate = _.defaultTo(metrics.rate, 0);
      const error_rate = _.defaultTo(metrics.error_rate, 0);
      const volume = rate + error_rate;

      let errorRate;
      if (rate >= 0 && error_rate >= 0) {
        errorRate = error_rate / rate;
      } else {
        errorRate = 0;
      }

      if (particles === undefined) {
        particles = {
          normal: [],
          danger: [],
        };
        edge.data('particles', particles);
      }

      if (metrics && volume > 0) {
        const spawnPropability = Math.min(volume / this.maxVolume, 1.0);
        for (let i = 0; i < 5; i++) {
          if (Math.random() <= spawnPropability + this.minSpawnPropability) {
            const particle: Particle = {
              velocity: 0.05 + Math.random() * 0.05,
              startTime: now,
            };
            if (Math.random() < errorRate) {
              particles.danger.push(particle);
            } else {
              particles.normal.push(particle);
            }
          }
        }
      }
    });
  }

  count() {
    const cy = this.drawer.cytoscape;

    const count = _(cy.edges())
      .map((edge) => edge.data('particles'))
      .filter()
      .map((particleArray) => particleArray.normal.length + particleArray.danger.length)
      .sum();

    return count;
  }
}


================================================
FILE: src/panel/layout_options.ts
================================================
const options = {
  name: 'cola',
  animate: true, // whether to show the layout as it's running
  refresh: 1, // number of ticks per frame; higher is faster but more jerky
  maxSimulationTime: 3000, // max length in ms to run the layout
  ungrabifyWhileSimulating: false, // so you can't drag nodes during layout
  fit: true, // set by controller // on every layout reposition of nodes, fit the viewport
  padding: 90, // padding around the simulation
  boundingBox: undefined as undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
  nodeDimensionsIncludeLabels: false, // whether labels should be included in determining the space used by a node

  // layout event callbacks
  ready: function () {}, // on layoutready
  stop: function () {}, // on layoutstop

  // positioning options
  randomize: false, // use random node positions at beginning of layout
  avoidOverlap: true, // if true, prevents overlap of node bounding boxes
  handleDisconnected: true, // if true, avoids disconnected components from overlapping
  convergenceThreshold: 0.01, // when the alpha value (system energy) falls below this value, the layout stops
  nodeSpacing: function (node: any) {
    return 50;
  }, // extra spacing around nodes
  flow: undefined as undefined, // use DAG/tree flow layout if specified, e.g. { axis: 'y', minSeparation: 30 }
  alignment: undefined as undefined, // relative alignment constraints on nodes, e.g. function( node ){ return { x: 0, y: 1 } }
  gapInequalities: undefined as undefined, // list of inequality constraints for the gap between the nodes, e.g. [{"axis":"y", "left":node1, "right":node2, "gap":25}]

  // different methods of specifying edge length
  // each can be a constant numerical value or a function like `function( edge ){ return 2; }`
  edgeLength: undefined as undefined, // sets edge length directly in simulation
  edgeSymDiffLength: undefined as undefined, // symmetric diff edge length in simulation
  edgeJaccardLength: undefined as undefined, // jaccard edge length in simulation

  // iterations of cola algorithm; uses default values on undefined
  unconstrIter: 50, // set by controller // unconstrained initial layout iterations
  userConstIter: undefined as undefined, // initial layout iterations with user-specified constraints
  allConstIter: undefined as undefined, // initial layout iterations with all constraints including non-overlap

  // infinite layout options
  infinite: false, // overrides all other options for a forces-all-the-time mode
};

export default options;


================================================
FILE: src/panel/serviceDependencyGraph/ServiceDependencyGraph.css
================================================
.service-dependency-graph-panel .graph-container {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: row;
}

.service-dependency-graph-panel .canvas-container {
    width: 100%;
    height: 100%;
    overflow: hidden;
}

.service-dependency-graph-panel .zoom-button-container {
    position: absolute;
    top: 0;
    right: 1rem;
    z-index: 99;
}


================================================
FILE: src/panel/serviceDependencyGraph/ServiceDependencyGraph.tsx
================================================
import CanvasDrawer from 'panel/canvas/graph_canvas';
import cytoscape, { EdgeCollection, EdgeSingular, ElementDefinition, NodeSingular } from 'cytoscape';
import React, { PureComponent } from 'react';
import { PanelController } from '../PanelController';
import cyCanvas from 'cytoscape-canvas';
import cola from 'cytoscape-cola';
import layoutOptions from '../layout_options';
import { Statistics } from '../statistics/Statistics';
import _ from 'lodash';
import {
  TableContent,
  IntGraphMetrics,
  IntGraph,
  IntGraphNode,
  IntGraphEdge,
  PanelSettings,
  IntSelectionStatistics,
} from 'types';
import { TemplateSrv, getTemplateSrv } from '@grafana/runtime';
import './ServiceDependencyGraph.css';

interface PanelState {
  zoom: number | undefined;
  animate: boolean | undefined;
  controller: PanelController;
  cy?: cytoscape.Core | undefined;
  graphCanvas?: CanvasDrawer | undefined;
  animateButtonClass?: string;
  showStatistics: boolean;
  data: IntGraph;
  settings: PanelSettings;
  layer: number | undefined;
  maxLayer: number;
  layerIncreaseFunction: any;
  layerDecreaseFunction: any;
}

cyCanvas(cytoscape);
cytoscape.use(cola);

export class ServiceDependencyGraph extends PureComponent<PanelState, PanelState> {
  ref: any;

  selectionId: string;

  currentType: string;

  selectionStatistics: IntSelectionStatistics;

  receiving: TableContent[];

  sending: TableContent[];

  resolvedDrillDownLink: string;

  templateSrv: TemplateSrv;

  initResize = true;

  constructor(props: PanelState) {
    super(props);

    let animateButtonClass = 'fa fa-play-circle';
    if (props.animate) {
      animateButtonClass = 'fa fa-pause-circle';
    }

    this.state = {
      ...props,
      showStatistics: false,
      animateButtonClass: animateButtonClass,
      animate: false,
    };

    this.ref = React.createRef();
    this.templateSrv = getTemplateSrv();
  }

  componentDidMount() {
    const cy: any = cytoscape({
      container: this.ref,
      zoom: this.state.zoom,
      elements: this.props.data,
      layout: {
        name: 'cola',
      },
      style: [
        {
          selector: 'node',
          css: {
            'background-color': '#fbfbfb',
            'background-opacity': 0,
          },
        },

        {
          selector: 'node:parent',
          css: {
            'background-opacity': 0.05,
            shape: 'barrel',
          },
        },

        {
          selector: 'edge',
          style: {
            'curve-style': 'bezier',
            'control-point-step-size': 100,
            visibility: 'hidden',
          },
        },
      ],
      wheelSensitivity: 0.125,
    });

    let graphCanvas = new CanvasDrawer(
      this,
      cy,
      cy.cyCanvas({
        zIndex: 1,
      })
    );

    cy.on('render cyCanvas.resize', () => {
      graphCanvas.repaint(true);
    });
    cy.on('select', 'node', () => this.onSelectionChange());
    cy.on('unselect', 'node', () => this.onSelectionChange());
    this.setState({
      cy: cy,
      graphCanvas: graphCanvas,
    });
    graphCanvas.start();
  }

  componentDidUpdate() {
    this._updateGraph(this.props.data);
  }

  _updateGraph(graph: IntGraph) {
    const cyNodes = this._transformNodes(graph.nodes);
    const cyEdges = this._transformEdges(graph.edges);

    const nodes = this.state.cy.nodes().toArray();
    const updatedNodes = this._updateOrRemove(nodes, cyNodes);

    // add new nodes
    this.state.cy.add(cyNodes);

    const edges = this.state.cy.edges().toArray();
    this._updateOrRemove(edges, cyEdges);

    // add new edges
    this.state.cy.add(cyEdges);

    if (this.initResize) {
      this.initResize = false;
      this.state.cy.resize();
      this.state.cy.reset();
      this.runLayout();
    } else {
      if (cyNodes.length > 0) {
        _.each(updatedNodes, (node) => {
          node.lock();
        });
        this.runLayout(true);
      }
    }
    this.state.graphCanvas.repaint(true);
  }

  _transformNodes(nodes: IntGraphNode[]): ElementDefinition[] {
    const cyNodes: ElementDefinition[] = _.map(nodes, (node) => {
      const result: ElementDefinition = {
        group: 'nodes',
        data: {
          id: node.data.id,
          type: node.data.type,
          external_type: node.data.external_type,
          parent: node.data.parent,
          layer: node.data.layer,
          metrics: {
            ...node.data.metrics,
          },
        },
      };
      return result;
    });

    return cyNodes;
  }

  _transformEdges(edges: IntGraphEdge[]): ElementDefinition[] {
    const cyEdges: ElementDefinition[] = _.map(edges, (edge) => {
      const cyEdge: ElementDefinition = {
        group: 'edges',
        data: {
          id: edge.data.source + ':' + edge.data.target,
          source: edge.data.source,
          target: edge.data.target,
          metrics: {
            ...edge.data.metrics,
          },
        },
      };

      return cyEdge;
    });

    return cyEdges;
  }

  _updateOrRemove(dataArray: Array<NodeSingular | EdgeSingular>, inputArray: ElementDefinition[]) {
    const elements: any[] = []; //(NodeSingular | EdgeSingular)[]
    for (let i = 0; i < dataArray.length; i++) {
      const element = dataArray[i];

      const cyNode = _.find(inputArray, { data: { id: element.id() } });

      if (cyNode) {
        element.data(cyNode.data);
        _.remove(inputArray, (n) => n.data.id === cyNode.data.id);
        elements.push(element);
      } else {
        element.remove();
      }
    }
    return elements;
  }

  onSelectionChange() {
    const selection = this.state.cy.$(':selected');

    if (selection.length === 1) {
      this.updateStatisticTable();
      this.setState({
        showStatistics: true,
      });
    } else {
      this.setState({
        showStatistics: false,
      });
    }
  }

  getSettings(resolveVariables: boolean): PanelSettings {
    return this.state.controller.getSettings(resolveVariables);
  }

  toggleAnimation() {
    let newValue = !this.state.animate;
    let animateButtonClass = 'fa fa-play-circle';
    if (newValue) {
      this.state.graphCanvas.startAnimation();
      animateButtonClass = 'fa fa-pause-circle';
    } else {
      this.state.graphCanvas.stopAnimation();
    }
    this.setState({
      animate: newValue,
      animateButtonClass: animateButtonClass,
    });
  }

  runLayout(unlockNodes = false) {
    const that = this;
    const options = {
      ...layoutOptions,

      stop: function () {
        if (unlockNodes) {
          that.unlockNodes();
        }
        that.setState({
          zoom: that.state.cy.zoom(),
        });
      },
    };

    this.state.cy.layout(options).run();
  }

  unlockNodes() {
    this.state.cy.nodes().forEach((node: { unlock: () => void }) => {
      node.unlock();
    });
  }

  fit() {
    const selection = this.state.graphCanvas.selectionNeighborhood;
    if (selection && !selection.empty()) {
      this.state.cy.fit(selection, 30);
    } else {
      this.state.cy.fit();
    }
    this.setState({
      zoom: this.state.cy.zoom(),
    });
  }

  zoom(zoom: number) {
    const zoomStep = 0.25 * zoom;
    const zoomLevel = Math.max(0.1, this.state.zoom + zoomStep);
    this.setState({
      zoom: zoomLevel,
    });
    this.state.cy.zoom(zoomLevel);
    this.state.cy.center();
  }

  updateStatisticTable() {
    const selection = this.state.cy.$(':selected');

    if (selection.length === 1) {
      const currentNode: NodeSingular = selection[0];
      this.selectionId = currentNode.id().toString();
      this.currentType = currentNode.data('type');
      const receiving: TableContent[] = [];
      const sending: TableContent[] = [];
      const edges: EdgeCollection = selection.connectedEdges();

      const metrics: IntGraphMetrics = selection.nodes()[0].data('metrics');

      const requestCount = _.defaultTo(metrics.rate, -1);
      const errorCount = _.defaultTo(metrics.error_rate, -1);
      const duration = _.defaultTo(metrics.response_time, -1);
      const threshold = _.defaultTo(metrics.threshold, -1);

      this.selectionStatistics = {};

      if (requestCount >= 0) {
        this.selectionStatistics.requests = Math.floor(requestCount);
      }
      if (errorCount >= 0) {
        this.selectionStatistics.errors = Math.floor(errorCount);
      }
      if (duration >= 0) {
        this.selectionStatistics.responseTime = Math.floor(duration);

        if (threshold >= 0) {
          this.selectionStatistics.threshold = Math.floor(threshold);
          this.selectionStatistics.thresholdViolation = duration > threshold;
        }
      }

      for (let i = 0; i < edges.length; i++) {
        const actualEdge: EdgeSingular = edges[i];
        const sendingCheck: boolean = actualEdge.source().id() === this.selectionId;
        let node: NodeSingular;

        if (sendingCheck) {
          node = actualEdge.target();
        } else {
          node = actualEdge.source();
        }

        const sendingObject: TableContent = {
          name: node.id(),
          responseTime: '-',
          rate: '-',
          error: '-',
        };

        const edgeMetrics: IntGraphMetrics = actualEdge.data('metrics');

        if (edgeMetrics !== undefined) {
          const { response_time, rate, error_rate } = edgeMetrics;

          if (rate !== undefined) {
            sendingObject.rate = Math.floor(rate).toString();
          }
          if (response_time !== undefined) {
            sendingObject.responseTime = Math.floor(response_time) + ' ms';
          }
          if (error_rate !== undefined && rate !== undefined) {
            sendingObject.error = Math.floor(error_rate / (rate / 100)) + '%';
          }
        }

        if (sendingCheck) {
          sending.push(sendingObject);
        } else {
          receiving.push(sendingObject);
        }
      }
      this.receiving = receiving;
      this.sending = sending;

      this.generateDrillDownLink();
    }
  }

  generateDrillDownLink() {
    const { drillDownLink } = this.getSettings(false);
    if (drillDownLink !== undefined) {
      const link = drillDownLink.replace('{}', this.selectionId);
      this.resolvedDrillDownLink = this.templateSrv.replace(link);
    }
  }

  render() {
    if (this.state.cy !== undefined) {
      this._updateGraph(this.props.data);
    }
    return (
      <div className="graph-container">
        <div className="service-dependency-graph">
          <div className="canvas-container" ref={(ref) => (this.ref = ref)}></div>
          <div className="zoom-button-container">
            <button className="btn navbar-button width-100" onClick={() => this.toggleAnimation()}>
              <i className={this.state.animateButtonClass}></i>
            </button>
            <button className="btn navbar-button width-100" onClick={() => this.runLayout()}>
              <i className="fa fa-sitemap"></i>
            </button>
            <button className="btn navbar-button width-100" onClick={() => this.fit()}>
              <i className="fa fa-dot-circle-o"></i>
            </button>
            <button className="btn navbar-button width-100" onClick={() => this.props.layerIncreaseFunction()}>
              <i className="fa fa-plus"></i>
            </button>
            <button className="btn navbar-button width-100" onClick={() => this.props.layerDecreaseFunction()}>
              <i className="fa fa-minus"></i>
            </button>
            <span>
              Layer {this.state.controller.state.currentLayer}/{this.state.maxLayer}
            </span>
          </div>
        </div>
        <Statistics
          show={this.state.showStatistics}
          selectionId={this.selectionId}
          resolvedDrillDownLink={this.resolvedDrillDownLink}
          selectionStatistics={this.selectionStatistics}
          currentType={this.currentType}
          showBaselines={this.getSettings(true).showBaselines}
          receiving={this.receiving}
          sending={this.sending}
        />
      </div>
    );
  }
}


================================================
FILE: src/panel/statistics/IncomingStatistics.tsx
================================================
import React from 'react';
import { TableContent } from 'types';

export const NodeStatistics = (receiving: TableContent[]) => {
  return (
    <>
      <div className="secondHeader--selection">Incoming Statistics</div>
      {() => {
        if (receiving.length > 0) {
          return (
            <table className="table--selection">
              <tr className="table--selection--head">
                <th>Name</th>
                <th className="table--th--selectionSmall">Time</th>
                <th className="table--th--selectionSmall">Requests</th>
                <th className="table--th--selectionSmall">Error Rate</th>
              </tr>
              {receiving.map((node: TableContent, index: number) => (
                <tr key={'row-' + index}>
                  <td className="table--td--selection" title="{{node.name}}">
                    {node.name}
                  </td>
                  <td className="table--td--selection">{node.responseTime}</td>
                  <td className="table--td--selection">{node.rate}</td>
                  <td className="table--td--selection">{node.error}</td>
                </tr>
              ))}
            </table>
          );
        }
        return <div className="no-data--selection">No incoming statistics available.</div>;
      }}
    </>
  );
};


================================================
FILE: src/panel/statistics/NodeStatistics.tsx
================================================
import React from 'react';
import { IntTableHeader } from '../../types';
import { TableContent } from 'types';
import SortableTable from './SortableTable';
import roundPercentageToDecimal from './utils/Utils';

interface NodeStatisticsProps {
  nodeList: TableContent[];
  noDataText: string;
  title: string;
}

const tableHeaders: IntTableHeader[] = [
  { text: 'Name', dataField: 'name', sort: true, isKey: true },
  { text: 'Time', dataField: 'time', sort: true, ignoreLiteral: ' ms' },
  { text: 'Requests', dataField: 'requests', sort: true, ignoreLiteral: '' },
  { text: 'Error Rate', dataField: 'error_rate', sort: true, ignoreLiteral: '%' },
];

function getStatisticsTable(noDataText: string, nodeList: TableContent[]) {
  if (nodeList.length > 0) {
    return (
      <SortableTable
        tableHeaders={tableHeaders}
        data={nodeList.map((node: TableContent) => {
          return {
            name: node.name,
            time: node.responseTime,
            requests: node.rate,
            error_rate: roundPercentageToDecimal(2, node.error),
          };
        })}
      />
    );
  } else {
    return <div className="no-data--selection">{noDataText}</div>;
  }
}

export const NodeStatistics: React.FC<NodeStatisticsProps> = ({ nodeList, noDataText, title }) => {
  return (
    <div>
      <div className="secondHeader--selection">{title}</div>
      {getStatisticsTable(noDataText, nodeList)}
    </div>
  );
};


================================================
FILE: src/panel/statistics/SortableTable.tsx
================================================
import React from 'react';
import { IntTableHeader, NodeData } from '../../types';
import BootstrapTable from 'react-bootstrap-table-next';

interface SortableTableProps {
  tableHeaders: IntTableHeader[];
  data: NodeData[];
}

function sort(a: string, b: string, order: string, ignoreLiteral: string) {
  let cleanA = a.replace(ignoreLiteral, '');
  let cleanB = b.replace(ignoreLiteral, '');
  if ((order === 'asc' && cleanA === '-') || (order !== 'asc' && cleanB === '-')) {
    return -1;
  }
  if ((order === 'asc' && cleanB === '-') || (order !== 'asc' && cleanA === '-')) {
    return 1;
  }
  if (order === 'asc') {
    return Number(cleanA) - Number(cleanB);
  }
  return Number(cleanB) - Number(cleanA);
}

export const SortableTable: React.FC<SortableTableProps> = ({ tableHeaders, data }) => {
  tableHeaders.forEach(function (value, i) {
    value.classes = 'table--td--selection';
    if (i !== 0) {
      value.sortFunc = (a: string, b: string, order: string, _dataField: any, _rowA: any) => {
        return sort(a, b, order, value.ignoreLiteral);
      };
    }
  });

  return (
    <BootstrapTable
      keyField="name"
      data={data}
      columns={tableHeaders}
      classes="table--selection"
      headerClasses="table--selection table--selection--head"
    />
  );
};

export default SortableTable;


================================================
FILE: src/panel/statistics/Statistics.css
================================================
.service-dependency-graph-panel .margin {
    margin: 10px;
}

.service-dependency-graph-panel .statistics {
    flex-basis: 0;
    transition: flex-basis 500ms ease-in-out;
    overflow-y: scroll;
}

.service-dependency-graph-panel .statistics.show {
  flex-basis: 30rem;
  padding-left: 0.5%;
}


================================================
FILE: src/panel/statistics/Statistics.tsx
================================================
import React from 'react';
import { NodeStatistics } from './NodeStatistics';
import '../../css/novatec-service-dependency-graph-panel.css';
import './Statistics.css';
import { IntSelectionStatistics, TableContent } from 'types';
import roundPercentageToDecimal from './utils/Utils';

interface StatisticsProps {
  show: boolean;
  selectionId: string | number;
  resolvedDrillDownLink: string;
  selectionStatistics: IntSelectionStatistics;
  currentType: string;
  showBaselines: boolean;
  receiving: TableContent[];
  sending: TableContent[];
}

export const Statistics: React.FC<StatisticsProps> = ({
  show,
  selectionId,
  resolvedDrillDownLink,
  selectionStatistics,
  currentType,
  showBaselines,
  receiving,
  sending,
}) => {
  let statisticsClass = 'statistics';
  let statistics = <div></div>;
  if (show) {
    statisticsClass = 'statistics show ';
    let drilldownLink = <div></div>;
    if (resolvedDrillDownLink && resolvedDrillDownLink.length > 0 && currentType === 'INTERNAL') {
      drilldownLink = (
        <a target="_blank" rel="noreferrer" href={resolvedDrillDownLink}>
          <i className="fa fa-paper-plane-o margin"></i>
        </a>
      );
    }

    const requests =
      selectionStatistics.requests >= 0 ? (
        <tr>
          <td className="table--td--selection">Requests</td>
          <td className="table--td--selection">{selectionStatistics.requests}</td>
        </tr>
      ) : null;

    const errors =
      selectionStatistics.errors >= 0 ? (
        <tr>
          <td className="table--td--selection">Errors</td>
          <td className="table--td--selection">{selectionStatistics.errors}</td>
        </tr>
      ) : null;

    let errorRate =
      selectionStatistics.requests > 0 && selectionStatistics.errors >= 0 ? (
        <tr>
          <td className="table--td--selection">Error Rate</td>
          <td className="table--td--selection">
            {roundPercentageToDecimal(
              2,
              ((100 / selectionStatistics.requests) * selectionStatistics.errors).toString()
            )}
          </td>
        </tr>
      ) : null;

    let avgResponseTime =
      selectionStatistics.responseTime >= 0 ? (
        <tr>
          <td className="table--td--selection">Avg. Response Time</td>
          <td className="table--td--selection">{selectionStatistics.responseTime} ms</td>
        </tr>
      ) : null;
    let threshold = selectionStatistics.thresholdViolation ? (
      <td className="table--td--selection threshold--bad">
        Bad ({'>'} {selectionStatistics.threshold}ms)
      </td>
    ) : (
      <td className="table--td--selection threshold--good"> Good (&lt;= {selectionStatistics.threshold}ms) </td>
    );
    let baseline =
      showBaselines && selectionStatistics.threshold ? (
        <tr>
          <td className="table--td--selection">Response Time Health (Upper Baseline)</td>
          {threshold}
        </tr>
      ) : null;

    statistics = (
      <div className="statistics">
        <div className="header--selection">
          {selectionId}
          {drilldownLink}
        </div>

        <div className="secondHeader--selection">Statistics</div>
        <table className="table--selection">
          <tr className="table--selection--head">
            <th>Name</th>
            <th className="table--th--selectionMedium">Value</th>
          </tr>
          {requests}
          {errors}
          {errorRate}
          {avgResponseTime}
          {baseline}
        </table>

        <NodeStatistics
          nodeList={receiving}
          noDataText="No incoming statistics available."
          title="Incoming Statistics"
        />
        <NodeStatistics nodeList={sending} noDataText="No outgoing statistics available." title="Outgoing Statistics" />
      </div>
    );
  }
  return <div className={statisticsClass}>{statistics}</div>;
};


================================================
FILE: src/panel/statistics/utils/Utils.ts
================================================
function roundPercentageToDecimal(decimal: number, value: string) {
  if (value !== '-') {
    let valueDecimals = _getDecimalsOf(parseFloat(value));
    if (valueDecimals > decimal) {
      value = parseFloat(value).toFixed(decimal) + '%';
    }
  }
  return value;
}

function _getDecimalsOf(value: number) {
  if (Math.floor(value) !== value) {
    return value.toString().split('.')[1].length || 0;
  }
  return 0;
}

export default roundPercentageToDecimal;


================================================
FILE: src/plugin.json
================================================
{
  "type": "panel",
  "name": "Service Dependency Graph",
  "id": "novatec-sdg-panel",
  "info": {
    "version": "4.2.0",
    "updated": "2025-03-26",
    "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.",
    "author": {
      "name": "Novatec Consulting GmbH",
      "url": "https://www.novatec-gmbh.de/"
    },
    "keywords": [
      "grafana",
      "plugin",
      "service-dependency-graph",
      "topology"
    ],
    "logos": {
      "small": "img/novatec_sdg_panel_logo.svg",
      "large": "img/novatec_sdg_panel_logo.svg"
    },
    "links": [
      {
        "name": "Project site",
        "url": "https://github.com/NovatecConsulting/novatec-service-dependency-graph-panel.git"
      },
      {
        "name": "Apache License",
        "url": "https://github.com/NovatecConsulting/novatec-service-dependency-graph-panel/blob/master/LICENSE"
      }
    ],
    "screenshots": [
      {
        "name": "Showcase",
        "path": "img/data-example-1.png"
      }
    ]
  },
  "dependencies": {
    "grafanaDependency": ">=10.4.0",
    "plugins": []
  }
}


================================================
FILE: src/processing/graph_generator.ts
================================================
import _ from 'lodash';
import { isPresent } from './utils/Utils';
import { PanelController } from '../panel/PanelController';
import {
  GraphDataElement,
  IntGraph,
  IntGraphEdge,
  IntGraphMetrics,
  IntGraphNode,
  EnGraphNodeType,
  GraphDataType,
} from '../types';
import NodeTree from './node_tree';
import NodeSubstitutor from './node_substitutor';

class GraphGenerator {
  controller: PanelController;
  nodeSubstitutor: NodeSubstitutor;

  constructor(controller: PanelController) {
    this.controller = controller;
    this.nodeSubstitutor = new NodeSubstitutor();
  }

  _createNode(dataElements: GraphDataElement[], nodeTree: NodeTree): IntGraphNode | undefined {
    if (!dataElements || dataElements.length <= 0) {
      return undefined;
    }

    const sumMetrics = this.controller.getSettings(true).sumTimings;

    let nodeName = dataElements[0].target;
    if (nodeName === '' || nodeName === undefined || nodeName === null) {
      nodeName = 'undefined';
    }

    const internalNode =
      _.some(dataElements, ['type', GraphDataType.INTERNAL]) ||
      _.some(dataElements, ['type', GraphDataType.EXTERNAL_IN]);
    const nodeType = internalNode ? EnGraphNodeType.INTERNAL : EnGraphNodeType.EXTERNAL;

    const metrics: IntGraphMetrics = {};

    const node: IntGraphNode = {
      data: {
        id: nodeName,
        label: nodeName,
        external_type: nodeType,
        type: nodeType,
        layer: 0,
        metrics,
        namespace: [],
      },
    };

    //get first element where namespace is defined.
    const namespaceElement = dataElements.find((el) => el.namespace !== undefined);
    if (namespaceElement) {
      const namespace = namespaceElement.namespace;
      node.data.namespace = namespace;
      node.data.layer = namespace.length;
      node.data.parent = namespace[namespace.length - 1];
      this._updateMaxLayer(node.data.layer);
    }

    const aggregationFunction = sumMetrics ? _.sum : _.mean;

    if (internalNode) {
      metrics.rate = _.sum(_.map(dataElements, (element) => element.data.rate_in));
      metrics.error_rate = _.sum(_.map(dataElements, (element) => element.data.error_rate_in));

      const response_timings = _.map(dataElements, (element) => element.data.response_time_in).filter(isPresent);
      if (response_timings.length > 0) {
        metrics.response_time = aggregationFunction(response_timings);
      }
    } else {
      metrics.rate = _.sum(_.map(dataElements, (element) => element.data.rate_out));
      metrics.error_rate = _.sum(_.map(dataElements, (element) => element.data.error_rate_out));

      const response_timings = _.map(dataElements, (element) => element.data.response_time_out).filter(isPresent);
      if (response_timings.length > 0) {
        metrics.response_time = aggregationFunction(response_timings);
      }

      const externalType = _(dataElements)
        .map((element) => element.data.type)
        .uniq()
        .value();

      if (externalType.length === 1) {
        node.data.external_type = externalType[0];
      }
    }

    // metrics which are same for internal and external nodes
    metrics.threshold = _(dataElements)
      .map((element) => element.data.threshold)
      .filter()
      .mean();

    if (sumMetrics) {
      const requestCount = _.defaultTo(metrics.rate, 0) + _.defaultTo(metrics.error_rate, 0);
      const response_time = _.defaultTo(metrics.response_time, -1);
      if (requestCount > 0 && response_time >= 0) {
        metrics.response_time = response_time / requestCount;
      }
    }

    const { rate, error_rate } = metrics;
    if (rate + error_rate > 0) {
      metrics.success_rate = (1.0 / (rate + error_rate)) * rate;
    } else {
      metrics.success_rate = 1.0;
    }

    nodeTree.addNode(node);
    this.nodeSubstitutor.add(node);
    return node;
  }

  _createMissingNodes(data: GraphDataElement[], nodes: IntGraphNode[]): IntGraphNode[] {
    const existingNodeNames = _.map(nodes, (node) => node.data.id);
    const expectedNodeNames = _.uniq(_.flatMap(data, (dataElement) => [dataElement.source, dataElement.target])).filter(
      isPresent
    );
    const missingNodeNames = _.difference(expectedNodeNames, existingNodeNames);
    const missingNodes = _.map(missingNodeNames, (name) => {
      let nodeType: EnGraphNodeType;
      let external_type: string | undefined;

      // derive node type
      let elementSrc = _.find(data, { source: name });
      let elementTrgt = _.find(data, { target: name });
      if (elementSrc && elementSrc.type === GraphDataType.EXTERNAL_IN) {
        nodeType = EnGraphNodeType.EXTERNAL;
        external_type = elementSrc.data.type;
      } else if (elementTrgt && elementTrgt.type === GraphDataType.EXTERNAL_OUT) {
        nodeType = EnGraphNodeType.EXTERNAL;
        external_type = elementTrgt.data.type;
      } else {
        nodeType = EnGraphNodeType.INTERNAL;
      }
      let value: IntGraphNode = {
        data: {
          id: name,
          type: nodeType,
          external_type: external_type,
          metrics: {},
          layer: 0,
        },
      };
      this.nodeSubstitutor.add(value);
      return value;
    });
    return missingNodes;
  }

  _createNodes(data: GraphDataElement[]): IntGraphNode[] {
    let tree = new NodeTree();
    const filteredData = _.filter(
      data,
      (dataElement) =>
        dataElement.source !== dataElement.target ||
        (_.has(dataElement, 'target') && !_.has(dataElement, 'target')) ||
        (!_.has(dataElement, 'target') && _.has(dataElement, 'target'))
    );

    const targetGroups = _.groupBy(filteredData, 'target');

    const explicitlyNamedNodes = _.map(targetGroups, (group) => this._createNode(group, tree)).filter(isPresent);

    // ensure that all nodes exist, even we have no data for them
    const missingNodes = this._createMissingNodes(filteredData, explicitlyNamedNodes);
    missingNodes.forEach((node) => tree.addNode(node));
    const allNodes = tree.getNodesFromLayer(this.controller.state.currentLayer);
    return allNodes;
  }

  _resolveSubstitute(name: string): string {
    return this.nodeSubstitutor.substituteUntilLayer(
      name,
      this.controller.state.currentLayer,
      this.controller.maxLayer
    );
  }

  _createEdge(dataElement: GraphDataElement): IntGraphEdge | undefined {
    let { source, target } = dataElement;
    if (source === undefined || target === undefined) {
      console.error('source and target are necessary to create an edge', dataElement);
      return undefined;
    }

    const metrics: IntGraphMetrics = {};

    source = this._resolveSubstitute(source);
    target = this._resolveSubstitute(target);
    if (source === target) {
      return undefined;
    }

    const edge: IntGraphEdge = {
      source: source,
      target: target,
      data: {
        source,
        target,
        metrics,
      },
    };

    const { rate_out, rate_in, error_rate_out, response_time_out } = dataElement.data;
    if (!_.isUndefined(rate_out)) {
      metrics.rate = rate_out;
    } else if (!_.isUndefined(rate_in)) {
      metrics.rate = rate_in;
    }
    if (!_.isUndefined(error_rate_out)) {
      metrics.error_rate = error_rate_out;
    }
    if (!_.isUndefined(response_time_out)) {
      const { sumTimings } = this.controller.getSettings(true);

      if (sumTimings && metrics.rate) {
        metrics.response_time = response_time_out / metrics.rate;
      } else {
        metrics.response_time = response_time_out;
      }
    }

    return edge;
  }

  _resolveEdgeMap(edges: IntGraphEdge[]) {
    let edgeMap: Map<string, IntGraphEdge[]> = new Map();
    edges.forEach((edge) => {
      if (edgeMap.get(edge.source + '-' + edge.target)) {
        edgeMap.get(edge.source + '-' + edge.target).push(edge);
      } else {
        edgeMap.set(edge.source + '-' + edge.target, [edge]);
      }
    });
    return edgeMap;
  }

  _mergeArrayOfEdges(edges: IntGraphEdge[]) {
    let errorRateCounter = 0;
    let rateCounter = 0;
    let responseTimeCounter = 0;
    let successRateCounter = 0;
    let thresholdCounter = 0;

    const mergedEdge: IntGraphEdge = {
      target: '',
      source: '',
      data: {
        source: '',
        target: '',
        metrics: {},
      },
    };
    edges.forEach((edge) => {
      if (mergedEdge.source === '') {
        mergedEdge.source = edge.source;
        mergedEdge.data.source = edge.data.source;
      }
      if (mergedEdge.target === '') {
        mergedEdge.target = edge.target;
        mergedEdge.data.target = edge.data.target;
      }
      if (edge.data.metrics.error_rate) {
        mergedEdge.data.metrics.error_rate = mergedEdge.data.metrics.error_rate
          ? mergedEdge.data.metrics.error_rate + edge.data.metrics.error_rate
          : (mergedEdge.data.metrics.error_rate = edge.data.metrics.error_rate);
        errorRateCounter++;
      }
      if (edge.data.metrics.rate) {
        mergedEdge.data.metrics.rate = mergedEdge.data.metrics.rate
          ? mergedEdge.data.metrics.rate + edge.data.metrics.rate
          : (mergedEdge.data.metrics.rate = edge.data.metrics.rate);
        rateCounter++;
      }
      if (edge.data.metrics.response_time) {
        mergedEdge.data.metrics.response_time = mergedEdge.data.metrics.response_time
          ? mergedEdge.data.metrics.response_time + edge.data.metrics.response_time
          : (mergedEdge.data.metrics.response_time = edge.data.metrics.response_time);
        responseTimeCounter++;
      }
      if (edge.data.metrics.success_rate) {
        mergedEdge.data.metrics.success_rate = mergedEdge.data.metrics.success_rate
          ? mergedEdge.data.metrics.success_rate + edge.data.metrics.success_rate
          : (mergedEdge.data.metrics.success_rate = edge.data.metrics.success_rate);
        successRateCounter++;
      }
      if (edge.data.metrics.threshold) {
        mergedEdge.data.metrics.threshold = mergedEdge.data.metrics.threshold
          ? mergedEdge.data.metrics.threshold + edge.data.metrics.threshold
          : (mergedEdge.data.metrics.threshold = edge.data.metrics.threshold);
        thresholdCounter++;
      }
    });

    if (mergedEdge.data.metrics.error_rate) {
      mergedEdge.data.metrics.error_rate = mergedEdge.data.metrics.error_rate / errorRateCounter;
    }
    if (mergedEdge.data.metrics.rate) {
      mergedEdge.data.metrics.rate = mergedEdge.data.metrics.rate / rateCounter;
    }
    if (mergedEdge.data.metrics.response_time) {
      mergedEdge.data.metrics.response_time = mergedEdge.data.metrics.response_time / responseTimeCounter;
    }
    if (mergedEdge.data.metrics.success_rate) {
      mergedEdge.data.metrics.success_rate = mergedEdge.data.metrics.success_rate / successRateCounter;
    }
    if (mergedEdge.data.metrics.threshold) {
      mergedEdge.data.metrics.threshold = mergedEdge.data.metrics.threshold / thresholdCounter;
    }

    return mergedEdge;
  }

  _edgeMapToMergedEdges(edgeMap: Map<string, IntGraphEdge[]>) {
    let edges: IntGraphEdge[] = [];
    for (const entry of edgeMap.values()) {
      edges.push(this._mergeArrayOfEdges(entry));
    }
    return edges;
  }

  _mergeEdges(edges: IntGraphEdge[]) {
    const edgeMap = this._resolveEdgeMap(edges);
    this._edgeMapToMergedEdges(edgeMap);

    return edges;
  }

  _createEdges(data: GraphDataElement[]): IntGraphEdge[] {
    const filteredData = _(data)
      .filter((e) => !!e.source)
      .filter((e) => e.source !== e.target)
      .filter((e) => e.target !== null || e.source !== null)
      .value();

    const edges = _.map(filteredData, (element) => this._createEdge(element));
    const filteredEdges = edges.filter(isPresent);
    return this._mergeEdges(filteredEdges);
  }

  _filterData(graph: IntGraph): IntGraph {
    const { filterEmptyConnections: filterData } = this.controller.getSettings(true);

    if (filterData) {
      const filteredGraph: IntGraph = {
        nodes: [],
        edges: [],
      };

      // filter empty connections
      filteredGraph.edges = _.filter(graph.edges, (edge) => _.size(edge.data.metrics) > 0);

      filteredGraph.nodes = _.filter(graph.nodes, (node) => {
        const id = node.data.id;

        // don't filter connected elements and parents
        if (
          _.some(graph.edges, { source: id }) ||
          _.some(graph.edges, { target: id }) ||
          node.data.type === EnGraphNodeType.PARENT
        ) {
          return true;
        }

        const metrics = node.data.metrics;
        if (!metrics) {
          return false; // no metrics
        }

        // only if rate, error rate or response time is available
        return (
          _.defaultTo(metrics.rate, -1) >= 0 ||
          _.defaultTo(metrics.error_rate, -1) >= 0 ||
          _.defaultTo(metrics.response_time, -1) >= 0
        );
      });

      return filteredGraph;
    } else {
      return graph;
    }
  }

  generateGraph(graphData: GraphDataElement[]): IntGraph {
    const nodes = this._createNodes(graphData);

    const edges = this._createEdges(graphData);
    const graph: IntGraph = {
      nodes,
      edges,
    };

    const filteredGraph = this._filterData(graph);
    return filteredGraph;
  }

  _updateMaxLayer(layer: number) {
    if (layer > this.controller.maxLayer) {
      this.controller.maxLayer = layer;
    }
  }
}

export default GraphGenerator;


================================================
FILE: src/processing/node_substitutor.ts
================================================
import _ from 'lodash';
import { IntGraphNode } from '../types';

class NodeSubstitutor {
  private _substitutionMap: any;

  constructor() {
    this._substitutionMap = new Map();
  }

  add(node: IntGraphNode) {
    const nameSpace = node.data.namespace;
    if (nameSpace && nameSpace.length > 0) {
      let currentValue = nameSpace;
      let currentKey = node.data.label;
      this._substitutionMap.set(currentKey, currentValue);
    }
  }

  substituteUntilLayer(nodeName: string, layer: number, maxLayer: number) {
    if (!this._substitutionMap.has(nodeName)) {
      return nodeName;
    }
    const nameSpace = this._substitutionMap.get(nodeName);
    const nsLeng = nameSpace.length;
    if (nsLeng - 1 < layer) {
      return nodeName;
    }

    return nameSpace[layer];
  }
}

export default NodeSubstitutor;


================================================
FILE: src/processing/node_tree.ts
================================================
import _ from 'lodash';
import { EnGraphNodeType, IntGraphMetrics, IntGraphNode, NodeTreeElement } from '../types';

class NodeTree {
  private _root: NodeTreeElement;
  private _metricMap: any;

  constructor() {
    this._root = { id: 'root', children: [] };
    this._metricMap = {};
  }

  addNode(node: IntGraphNode) {
    if (node.data.id !== 'undefined') {
      this._addNode({ id: node.data.id, node: node, children: [] }, this._root, 0);
    }
  }

  getNodesFromLayer(layer: number) {
    let nodes = this._getNodesFromLayer(this._root, layer, 0);
    nodes.forEach((element) => {
      if (this._metricMap[element.data.id]) {
        (Object.keys(element.data.metrics) as Array<keyof typeof element.data.metrics>).forEach(
          (key) => (element.data.metrics[key] = element.data.metrics[key] / this._metricMap[element.data.id][key])
        );
      }
    });
    return this._getNodesFromLayer(this._root, layer, 0);
  }

  getNamePath(namePath: string[]) {
    let currentLayer = this._root;
    namePath.forEach((element) => {
      currentLayer = this._getObjectFromArray(currentLayer.children, element);
    });
    return currentLayer;
  }

  private _getNodesFromLayer(currentNode: NodeTreeElement, layer: number, layerCounter: number): IntGraphNode[] {
    let children;
    if (layer === layerCounter) {
      children = currentNode.children.map((element) => element.node);
      if (currentNode !== this._root) {
        children.push(currentNode.node);
      }
      return children;
    }
    layerCounter++;
    children = _.flatten(currentNode.children.map((element) => this._getNodesFromLayer(element, layer, layerCounter)));
    if (currentNode !== this._root) {
      children.push(currentNode.node);
    }
    return children;
  }

  private _getNameSpaceFromCurrentLevel(namespace: string[], currentLevel: number) {
    let nameSpaces = [];
    for (let i = 0; i < currentLevel; i++) {
      nameSpaces.push(namespace[i]);
    }
    return nameSpaces;
  }

  private _sumMetrics(sourceNode: IntGraphNode, targetNode: IntGraphNode): IntGraphMetrics {
    const source = sourceNode.data.metrics;
    const target = targetNode.data.metrics;
    let metrics: IntGraphMetrics = {};
    if (!this._metricMap[targetNode.data.id]) {
      this._metricMap[targetNode.data.id] = {};
    }
    if (target.rate || source.rate) {
      metrics.rate = (target.rate ? target.rate : 0) + (source.rate ? source.rate : 0);
      if (target.rate && source.rate && !isNaN(target.rate) && !isNaN(source.rate)) {
        this._metricMap[targetNode.data.id].rate
          ? (this._metricMap[targetNode.data.id].rate = this._metricMap[targetNode.data.id].rate + 1)
          : (this._metricMap[targetNode.data.id].rate = 1);
      } else {
        if (!this._metricMap[targetNode.data.id].rate) {
          this._metricMap[targetNode.data.id].rate = 1;
        }
      }
    }

    if (target.response_time || source.response_time) {
      metrics.response_time =
        (target.response_time ? target.response_time : 0) + (source.response_time ? source.response_time : 0);
      if (
        target.response_time &&
        source.response_time &&
        !isNaN(target.response_time) &&
        !isNaN(source.response_time)
      ) {
        this._metricMap[targetNode.data.id].response_time
          ? (this._metricMap[targetNode.data.id].response_time = this._metricMap[targetNode.data.id].response_time + 1)
          : (this._metricMap[targetNode.data.id].response_time = 1);
      } else {
        if (!this._metricMap[targetNode.data.id].response_time) {
          this._metricMap[targetNode.data.id].response_time = 1;
        }
      }
    }

    if (target.success_rate || source.success_rate) {
      metrics.success_rate =
        (target.success_rate ? target.success_rate : 0) + (source.success_rate ? source.success_rate : 0);
      if (target.success_rate && source.success_rate && !isNaN(target.success_rate) && !isNaN(source.success_rate)) {
        this._metricMap[targetNode.data.id].success_rate
          ? (this._metricMap[targetNode.data.id].success_rate = this._metricMap[targetNode.data.id].success_rate + 1)
          : (this._metricMap[targetNode.data.id].success_rate = 1);
      } else {
        if (!this._metricMap[targetNode.data.id].success_rate) {
          this._metricMap[targetNode.data.id].success_rate = 1;
        }
      }
    }
    return metrics;
  }

  private _getObjectFromArray(array: NodeTreeElement[], id: string) {
    for (let i = 0; i < array.length; i++) {
      if (array[i].node.data.label === id) {
        return array[i];
      }
    }
    return undefined;
  }

  private _addNode(nodeToAdd: NodeTreeElement, currentLayerNode: NodeTreeElement, currentLevel: number) {
    const namespace = nodeToAdd.node.data.namespace;
    const namespaceLength = namespace ? namespace.length : 0;
    if (namespaceLength === currentLevel) {
      const possibleDuplicate = _.find(currentLayerNode.children, function (o) {
        return o.id === nodeToAdd.id;
      });
      if (possibleDuplicate) {
        //Object.assign(possibleDuplicate.node.data.metrics, nodeToAdd.node.data.metrics);
        //TODO: Copy/merge metrics
      } else {
        currentLayerNode.children.push(nodeToAdd);
      }
    } else {
      const nextLayerNode = this._getObjectFromArray(
        currentLayerNode.children,
        nodeToAdd.node.data.namespace[currentLevel]
      );
      if (nextLayerNode === undefined) {
        const children: NodeTreeElement[] = [];
        const newNode = {
          id: nodeToAdd.node.data.namespace[currentLevel],
          children: children,
          node: {
            data: {
              id: nodeToAdd.node.data.namespace[currentLevel],
              type: EnGraphNodeType.PARENT,
              label: nodeToAdd.node.data.namespace[currentLevel],
              parent: nodeToAdd.node.data.namespace[currentLevel - 1],
              namespace: this._getNameSpaceFromCurrentLevel(nodeToAdd.node.data.namespace, currentLevel),
              layer: currentLevel,
              metrics: {},
            },
          },
        };
        currentLayerNode.children.push(newNode);
        currentLevel++;
        newNode.node.data.metrics = this._sumMetrics(nodeToAdd.node, newNode.node);
        this._addNode(nodeToAdd, newNode, currentLevel);
      } else {
        const nextTopLayerNode = this._getObjectFromArray(
          currentLayerNode.children,
          nodeToAdd.node.data.namespace[currentLevel]
        );
        nextTopLayerNode.node.data.type = EnGraphNodeType.PARENT;
        nextTopLayerNode.node.data.metrics = this._sumMetrics(nodeToAdd.node, nextTopLayerNode.node);
        currentLevel++;
        this._addNode(nodeToAdd, nextTopLayerNode, currentLevel);
      }
    }
  }
}

export default NodeTree;


================================================
FILE: src/processing/pre_processor.ts
================================================
import { DataFrame } from '@grafana/data';
import _ from 'lodash';
import { PanelController } from '../panel/PanelController';
import { GraphDataElement, GraphDataType, CurrentData } from '../types';

class PreProcessor {
  controller: PanelController;

  constructor(controller: PanelController) {
    this.controller = controller;
  }

  _transformObjects(data: any[]): GraphDataElement[] {
    const {
      aggregationType,
      sourceColumn,
      targetColumn,
      extOrigin: externalSource,
      extTarget: externalTarget,
      namespaceDelimiter,
    } = this.controller.getSettings(true).dataMapping;

    const result = _.map(data, (dataObject) => {
      let source = _.has(dataObject, sourceColumn) && dataObject[sourceColumn] !== '';
      let target = _.has(dataObject, targetColumn) && dataObject[targetColumn] !== '';
      const extSource = _.has(dataObject, externalSource) && dataObject[externalSource] !== '';
      const extTarget = _.has(dataObject, externalTarget) && dataObject[externalTarget] !== '';

      let trueCount = [source, target, extSource, extTarget].filter((e) => e).length;

      if (trueCount > 1) {
        if (target && extTarget) {
          target = false;
        } else if (source && extSource) {
          source = false;
        } else {
          console.error('source-target conflict for data element', dataObject);
          return undefined;
        }
      }

      const result: GraphDataElement = {
        target: '',
        data: dataObject,
        type: GraphDataType.INTERNAL,
      };

      if (_.has(dataObject, 'namespace')) {
        const nameSpace = _.get(dataObject, 'namespace');
        if (nameSpace) {
          const namespaceResolved = nameSpace.split(namespaceDelimiter);
          result.namespace = namespaceResolved;
        }
      }

      if (trueCount === 0) {
        result.target = dataObject[aggregationType];
        result.type = GraphDataType.EXTERNAL_IN;
      } else {
        if (source || target) {
          if (source) {
            result.source = dataObject[sourceColumn];
            result.target = dataObject[aggregationType];
          } else {
            result.source = dataObject[aggregationType];
            result.target = dataObject[targetColumn];
          }

          if (result.source === result.target) {
            result.type = GraphDataType.SELF;
          }
        } else if (extSource) {
          result.source = dataObject[externalSource];
          result.target = dataObject[aggregationType];
          result.type = GraphDataType.EXTERNAL_IN;
        } else if (extTarget) {
          result.source = dataObject[aggregationType];
          result.target = dataObject[externalTarget];
          result.type = GraphDataType.EXTERNAL_OUT;
        }
      }
      return result;
    });

    const filteredResult: GraphDataElement[] = result.filter(
      (element): element is GraphDataElement => element !== null
    );
    return filteredResult;
  }

  _mergeGraphData(data: GraphDataElement[]): GraphDataElement[] {
    const groupedData = _.values(_.groupBy(data, (element) => element.source + '<--->' + element.target));

    const mergedData = _.map(groupedData, (group) => {
      return _.reduce(group, (result, next) => {
        return _.merge(result, next);
      });
    });

    return mergedData;
  }

  _cleanMetaData(columnMapping: any, metaData: any) {
    const result: any = {};

    _.forOwn(columnMapping, (value, key) => {
      if (_.has(metaData, value)) {
        result[key] = metaData[value];
      }
    });

    return result;
  }

  _extractColumnNames(data: GraphDataElement[]): string[] {
    const columnNames: string[] = _(data)
      .flatMap((dataElement) => _.keys(dataElement.data))
      .uniq()
      .sort()
      .value();

    return columnNames;
  }

  _getField(fieldName: string, fields: any[]) {
    for (const field of fields) {
      if (field.name === fieldName) {
        return field;
      }
    }
    return undefined;
  }

  _mergeSeries(series: any[]) {
    let mergedSeries: any = undefined;
    for (const seriesElement of series) {
      if (mergedSeries === undefined) {
        mergedSeries = seriesElement;
      } else {
        for (const field of seriesElement.fields) {
          const mergedField = this._getField(field.name, mergedSeries.fields);
          if (mergedField === undefined) {
            mergedSeries.fields.push(field);
          } else {
            mergedField.values = _.concat(field.values, mergedField.values);
          }
        }
      }
    }
    return mergedSeries;
  }

  _dataToRows(inputDataSets: any) {
    let rows: any[] = [];

    const {
      aggregationType,
      sourceColumn,
      targetColumn,
      namespaceColumn,
      extOrigin,
      extTarget,
      type,
      errorRateColumn,
      errorRateOutgoingColumn,
      responseTimeColumn,
      responseTimeOutgoingColumn,
      requestRateColumn,
      requestRateOutgoingColumn,
      baselineRtUpper,
    } = this.controller.getSettings(true).dataMapping;

    for (const inputData of inputDataSets) {
      const { fields } = inputData;
      const externalSourceField = _.find(fields, ['name', extOrigin]);
      const externalTargetField = _.find(fields, ['name', extTarget]);
      const aggregationSuffixField = _.find(fields, ['name', aggregationType]);

      const typeField = _.find(fields, ['name', type]);

      const sourceColumnField = _.find(fields, ['name', sourceColumn]);
      const targetColumnField = _.find(fields, ['name', targetColumn]);
      const namespaceColumnField = _.find(fields, ['name', namespaceColumn]);

      const errorRateColumnField = _.find(fields, ['name', errorRateColumn]);
      const errorRateOutgoingColumnField = _.find(fields, ['name', errorRateOutgoingColumn]);
      const responseTimeColumnField = _.find(fields, ['name', responseTimeColumn]);
      const responseTimeOutgoingColumnField = _.find(fields, ['name', responseTimeOutgoingColumn]);
      const requestRateColumnField = _.find(fields, ['name', requestRateColumn]);
      const requestRateOutgoingColumnField = _.find(fields, ['name', requestRateOutgoingColumn]);
      const responseTimeBaselineField = _.find(fields, ['name', baselineRtUpper]);

      for (let i = 0; i < inputData.length; i++) {
        const row: any = {};
        row[extOrigin] = externalSourceField?.values.get(i);
        row[extTarget] = externalTargetField?.values.get(i);
        row[aggregationType] = aggregationSuffixField?.values.get(i);
        row[sourceColumn] = sourceColumnField?.values.get(i);
        row[targetColumn] = targetColumnField?.values.get(i);
        row['namespace'] = namespaceColumnField?.values.get(i);
        row['error_rate_in'] = errorRateColumnField?.values.get(i);
        row['error_rate_out'] = errorRateOutgoingColumnField?.values.get(i);
        row['response_time_in'] = responseTimeColumnField?.values.get(i);
        row['response_time_out'] = responseTimeOutgoingColumnField?.values.get(i);
        row['rate_in'] = requestRateColumnField?.values.get(i);
        row['rate_out'] = requestRateOutgoingColumnField?.values.get(i);
        row['threshold'] = responseTimeBaselineField?.values.get(i);
        row['type'] = typeField?.values.get(i);
        // The above code returns { "": undefined } for values that do not exist.
        // These values are filtered by this line.
        Object.keys(row).forEach((key) => (row[key] === undefined || row[key] === '') && delete row[key]);
        rows.push(row);
      }
    }
    return rows;
  }

  _resolveData(row: any) {
    let source = _.has(row, 'sourceColumn') && row['sourceColumn'] !== '';
    let target = _.has(row, 'targetColumn') && row['targetColumn'] !== '';
    const extSource = _.has(row, 'extOrigin') && row['extOrigin'] !== '';
    const extTarget = _.has(row, 'extTarget') && row['extTarget'] !== '';
    let trueCount = [source, target, extSource, extTarget].filter((e) => e).length;

    if (trueCount > 1) {
      if (target && extTarget) {
        target = false;
      } else if (source && extSource) {
        source = false;
      } else {
        console.error('source-target conflict for data element', row);
        return;
      }
    }
    let resolvedObject: any = {
      data: row.data,
    };
    if (trueCount === 0) {
      resolvedObject.target = row['aggregationSuffix'];
      resolvedObject.type = GraphDataType.EXTERNAL_IN;
    } else {
      if (source || target) {
        if (source) {
          resolvedObject.source = row['sourceColumn'];
          resolvedObject.target = row['aggregationSuffix'];
          resolvedObject.type = GraphDataType.INTERNAL;
        } else {
          resolvedObject.source = row['aggregationSuffix'];
          resolvedObject.target = row['targetColumn'];
          resolvedObject.type = GraphDataType.INTERNAL;
        }

        if (resolvedObject.source === resolvedObject.target) {
          resolvedObject.type = GraphDataType.SELF;
        }
      } else if (extSource) {
        resolvedObject.source = row['externalSource'];
        resolvedObject.target = row['aggregationSuffix'];
        resolvedObject.type = GraphDataType.EXTERNAL_IN;
      } else if (extTarget) {
        resolvedObject.source = row['aggregationSuffix'];
        resolvedObject.target = row['externalTarget'];
        resolvedObject.type = GraphDataType.EXTERNAL_OUT;
      }
    }
    return resolvedObject;
  }

  _mergeObjects(rows: any[]) {
    let mergedObjects: any[] = [];

    for (const row of rows) {
      mergedObjects.push(row);
    }
    return mergedObjects;
  }

  processData(inputData: DataFrame[]): CurrentData {
    const rows = this._dataToRows(inputData);

    const flattenData = this._mergeObjects(rows);

    const graphElements = this._transformObjects(flattenData);

    const columnNames = this._extractColumnNames(graphElements);

    const mergedData = this._mergeGraphData(graphElements);

    return {
      graph: mergedData,
      raw: inputData,
      columnNames: columnNames,
    };
  }
}

export default PreProcessor;


================================================
FILE: src/processing/utils/Utils.ts
================================================
import _ from 'lodash';
import { DataMapping } from '../../types';
import { ServiceDependencyGraph } from 'panel/serviceDependencyGraph/ServiceDependencyGraph';

export function isPresent<T>(t: T | undefined | null | void): t is T {
  return t !== undefined && t !== null;
}

export default {
  getConfig: function (graph: ServiceDependencyGraph, configName: keyof DataMapping) {
    return graph.getSettings(true).dataMapping[configName];
  },
};


================================================
FILE: src/types.tsx
================================================
import { DataFrame } from '@grafana/data';

export interface PanelSettings {
  animate: boolean;
  sumTimings: boolean;
  filterEmptyConnections: boolean;
  style: PanelStyleSettings;
  showDebugInformation: boolean;
  showConnectionStats: boolean;
  icons: IconResource[];
  externalIcons: IconResource[];
  dataMapping: DataMa
Download .txt
gitextract_lgiult8p/

├── .circleci/
│   └── config.yml
├── .codeclimate.yml
├── .config/
│   ├── .eslintrc
│   ├── .prettierrc.js
│   ├── Dockerfile
│   ├── README.md
│   ├── jest/
│   │   ├── mocks/
│   │   │   └── react-inlinesvg.tsx
│   │   └── utils.js
│   ├── jest-setup.js
│   ├── jest.config.js
│   ├── tsconfig.json
│   ├── types/
│   │   └── custom.d.ts
│   └── webpack/
│       ├── constants.ts
│       ├── utils.ts
│       └── webpack.config.ts
├── .editorconfig
├── .eslintrc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── feature_request.md
│   │   └── question.md
│   └── workflows/
│       └── build.yml
├── .gitignore
├── .nvmrc
├── .prettierrc.js
├── .release-it.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── appveyor.yml
├── docker-compose.yaml
├── docs/
│   └── README.md
├── jest-setup.js
├── jest.config.js
├── package.json
├── provisioning/
│   ├── dashboards/
│   │   └── dashboards.yml
│   └── home/
│       └── home.json
├── scripts/
│   └── create-signed-plugin.sh
├── src/
│   ├── assets/
│   │   └── icons/
│   │       └── icon_index.json
│   ├── css/
│   │   └── novatec-service-dependency-graph-panel.css
│   ├── dummy_data_frame.ts
│   ├── migration/
│   │   └── PanelMigration.tsx
│   ├── module.test.ts
│   ├── module.ts
│   ├── options/
│   │   ├── DefaultSettings.tsx
│   │   ├── TypeAheadTextfield/
│   │   │   ├── TypeaheadTextfield.css
│   │   │   └── TypeaheadTextfield.tsx
│   │   ├── dummyDataSwitch/
│   │   │   └── DummyDataSwitch.tsx
│   │   ├── iconMapping/
│   │   │   ├── IconMapping.css
│   │   │   └── IconMapping.tsx
│   │   └── options.tsx
│   ├── panel/
│   │   ├── PanelController.tsx
│   │   ├── asset_utils.tsx
│   │   ├── canvas/
│   │   │   ├── collision_detector.ts
│   │   │   ├── graph_canvas.ts
│   │   │   └── particle_engine.ts
│   │   ├── layout_options.ts
│   │   ├── serviceDependencyGraph/
│   │   │   ├── ServiceDependencyGraph.css
│   │   │   └── ServiceDependencyGraph.tsx
│   │   └── statistics/
│   │       ├── IncomingStatistics.tsx
│   │       ├── NodeStatistics.tsx
│   │       ├── SortableTable.tsx
│   │       ├── Statistics.css
│   │       ├── Statistics.tsx
│   │       └── utils/
│   │           └── Utils.ts
│   ├── plugin.json
│   ├── processing/
│   │   ├── graph_generator.ts
│   │   ├── node_substitutor.ts
│   │   ├── node_tree.ts
│   │   ├── pre_processor.ts
│   │   └── utils/
│   │       └── Utils.ts
│   ├── types.tsx
│   └── typings/
│       └── index.d.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (197 symbols across 23 files)

FILE: .config/jest/mocks/react-inlinesvg.tsx
  type Callback (line 7) | type Callback = (...args: any[]) => void;
  type StorageItem (line 9) | interface StorageItem {
  constant SVG_FILE_NAME_REGEX (line 17) | const SVG_FILE_NAME_REGEX = /(.+)\/(.+)\.svg$/;

FILE: .config/webpack/constants.ts
  constant SOURCE_DIR (line 1) | const SOURCE_DIR = 'src';
  constant DIST_DIR (line 2) | const DIST_DIR = 'dist';

FILE: .config/webpack/utils.ts
  function isWSL (line 10) | function isWSL() {
  function getPackageJson (line 26) | function getPackageJson() {
  function getPluginJson (line 30) | function getPluginJson() {
  function hasReadme (line 34) | function hasReadme() {
  function getEntries (line 40) | async function getEntries(): Promise<Record<string, string>> {

FILE: src/migration/PanelMigration.tsx
  function isLegacyFormat (line 9) | function isLegacyFormat(options: any) {
  function migrateIconMapping (line 17) | function migrateIconMapping(iconMappings: any) {

FILE: src/options/TypeAheadTextfield/TypeaheadTextfield.tsx
  type Props (line 6) | interface Props extends StandardEditorProps<string, PanelSettings> {
  type State (line 12) | interface State {
  class TypeaheadTextField (line 19) | class TypeaheadTextField extends React.PureComponent<Props, State> {
    method constructor (line 20) | constructor(props: Props | Readonly<Props>) {
    method renderSuggestion (line 32) | renderSuggestion(suggestion: string) {
    method getColumnNames (line 35) | getColumnNames() {
    method render (line 89) | render() {

FILE: src/options/dummyDataSwitch/DummyDataSwitch.tsx
  type Props (line 6) | interface Props extends StandardEditorProps<boolean, PanelSettings> {
  type State (line 13) | interface State {
  class DummyDataSwitch (line 21) | class DummyDataSwitch extends React.PureComponent<Props, State> {
    method constructor (line 22) | constructor(props: Props | Readonly<Props>) {
    method render (line 68) | render() {

FILE: src/options/iconMapping/IconMapping.tsx
  type Props (line 7) | interface Props extends StandardEditorProps<string, PanelSettings> {
  type State (line 14) | interface State {
  class IconMapping (line 22) | class IconMapping extends React.PureComponent<Props, State> {
    method constructor (line 23) | constructor(props: Props | Readonly<Props>) {
    method addMapping (line 44) | addMapping() {
    method removeMapping (line 51) | removeMapping(index: number) {
    method setPatternValue (line 58) | setPatternValue(event: React.ChangeEvent<HTMLInputElement>, index: num...
    method setFileNameValue (line 65) | setFileNameValue(event: ChangeEvent<HTMLSelectElement>, index: number) {
    method render (line 72) | render() {

FILE: src/panel/PanelController.tsx
  type Props (line 20) | interface Props extends PanelProps<PanelSettings> {}
  type PanelState (line 22) | interface PanelState {
  class PanelController (line 40) | class PanelController extends PureComponent<Props, PanelState> {
    method constructor (line 55) | constructor(props: Props) {
    method getSettings (line 66) | getSettings(resolveVariables: boolean): PanelSettings {
    method resolveVariables (line 73) | resolveVariables(element: any) {
    method resolveTemplateVars (line 88) | resolveTemplateVars(input: any, copy: boolean) {
    method componentDidUpdate (line 105) | componentDidUpdate() {
    method processQueryData (line 109) | processQueryData(data: DataFrame[]) {
    method hasOnlyTableQueries (line 116) | hasOnlyTableQueries(inputData: DataFrame[]) {
    method processData (line 128) | processData() {
    method _transformEdges (line 138) | _transformEdges(edges: IntGraphEdge[]): CyData[] {
    method _transformNodes (line 157) | _transformNodes(nodes: IntGraphNode[]): CyData[] {
    method _updateOrRemove (line 179) | _updateOrRemove(dataArray: Array<NodeSingular | EdgeSingular>, inputAr...
    method getError (line 197) | getError(): string | null {
    method isDataAvailable (line 204) | isDataAvailable() {
    method layer (line 210) | layer(layerIncrease: number) {
    method render (line 222) | render() {

FILE: src/panel/asset_utils.tsx
  method getAssetUrl (line 5) | getAssetUrl(assetName: string) {
  method getTypeSymbol (line 10) | getTypeSymbol(type: string, externalIcons: IconResource[], resolveName =...

FILE: src/panel/canvas/collision_detector.ts
  class CollisionDetector (line 4) | class CollisionDetector {
    method constructor (line 7) | constructor() {
    method reset (line 11) | reset() {
    method addRectangle (line 15) | addRectangle(x: number, y: number, width: number, height: number) {
    method isColliding (line 27) | isColliding(shape: Rectangle) {
    method _intersects (line 37) | _intersects(a: Rectangle, b: Rectangle) {
    method _getBottomRightCorner (line 52) | _getBottomRightCorner(rectangle: Rectangle) {

FILE: src/panel/canvas/graph_canvas.ts
  class CanvasDrawer (line 26) | class CanvasDrawer {
    method constructor (line 72) | constructor(ctrl: ServiceDependencyGraph, cy: cytoscape.Core, cyCanvas...
    method _getTimeScale (line 95) | _getTimeScale(timeUnit: string) {
    method resetAssets (line 106) | resetAssets() {
    method _loadImage (line 110) | _loadImage(imageUrl: string, assetName: string) {
    method _isImageLoaded (line 131) | _isImageLoaded(assetName: string) {
    method _getImageAsset (line 139) | _getImageAsset(assetName: string, resolveName = true) {
    method _getAsset (line 153) | _getAsset(assetName: string, relativeUrl: string) {
    method start (line 166) | start() {
    method startAnimation (line 181) | startAnimation() {
    method stopAnimation (line 185) | stopAnimation() {
    method _skipFrame (line 190) | _skipFrame() {
    method repaint (line 204) | repaint(forceRepaint = false) {
    method _setTransformation (line 256) | _setTransformation(ctx: CanvasRenderingContext2D) {
    method _drawEdgeAnimation (line 264) | _drawEdgeAnimation(ctx: CanvasRenderingContext2D) {
    method _drawEdges (line 282) | _drawEdges(ctx: CanvasRenderingContext2D, edges: cytoscape.EdgeSingula...
    method _drawEdgeLine (line 300) | _drawEdgeLine(
    method _drawEdgeLabel (line 338) | _drawEdgeLabel(ctx: CanvasRenderingContext2D, edge: cytoscape.EdgeSing...
    method _drawEdgeParticles (line 372) | _drawEdgeParticles(
    method _drawLabel (line 435) | _drawLabel(ctx: CanvasRenderingContext2D, label: string, cX: number, c...
    method _getNextPointOnVector (line 472) | _getNextPointOnVector(x: number, y: number, edge: cytoscape.EdgeSingul...
    method _drawParticle (line 484) | _drawParticle(drawCtx: DrawContext, particles: Particle[], index: numb...
    method _drawNodes (line 503) | _drawNodes(ctx: CanvasRenderingContext2D) {
    method _drawNode (line 536) | _drawNode(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSingular) {
    method _drawServiceIcon (line 585) | _drawServiceIcon(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSi...
    method _drawNodeStatistics (line 609) | _drawNodeStatistics(ctx: CanvasRenderingContext2D, node: cytoscape.Nod...
    method _drawThresholdStroke (line 646) | _drawThresholdStroke(
    method _drawExternalService (line 692) | _drawExternalService(ctx: CanvasRenderingContext2D, node: cytoscape.No...
    method _drawNodeLabel (line 716) | _drawNodeLabel(ctx: CanvasRenderingContext2D, node: cytoscape.NodeSing...
    method _drawDebugInformation (line 757) | _drawDebugInformation() {
    method _drawDonut (line 768) | _drawDonut(
    method _drawArc (line 809) | _drawArc(

FILE: src/panel/canvas/particle_engine.ts
  class ParticleEngine (line 5) | class ParticleEngine {
    method constructor (line 16) | constructor(canvasDrawer: CanvasDrawer) {
    method start (line 21) | start() {
    method stop (line 29) | stop() {
    method animate (line 33) | animate() {
    method hasParticles (line 46) | hasParticles() {
    method _spawnParticles (line 58) | _spawnParticles() {
    method count (line 108) | count() {

FILE: src/panel/serviceDependencyGraph/ServiceDependencyGraph.tsx
  type PanelState (line 22) | interface PanelState {
  class ServiceDependencyGraph (line 41) | class ServiceDependencyGraph extends PureComponent<PanelState, PanelStat...
    method constructor (line 60) | constructor(props: PanelState) {
    method componentDidMount (line 79) | componentDidMount() {
    method componentDidUpdate (line 136) | componentDidUpdate() {
    method _updateGraph (line 140) | _updateGraph(graph: IntGraph) {
    method _transformNodes (line 172) | _transformNodes(nodes: IntGraphNode[]): ElementDefinition[] {
    method _transformEdges (line 193) | _transformEdges(edges: IntGraphEdge[]): ElementDefinition[] {
    method _updateOrRemove (line 213) | _updateOrRemove(dataArray: Array<NodeSingular | EdgeSingular>, inputAr...
    method onSelectionChange (line 231) | onSelectionChange() {
    method getSettings (line 246) | getSettings(resolveVariables: boolean): PanelSettings {
    method toggleAnimation (line 250) | toggleAnimation() {
    method runLayout (line 265) | runLayout(unlockNodes = false) {
    method unlockNodes (line 283) | unlockNodes() {
    method fit (line 289) | fit() {
    method zoom (line 301) | zoom(zoom: number) {
    method updateStatisticTable (line 311) | updateStatisticTable() {
    method generateDrillDownLink (line 393) | generateDrillDownLink() {
    method render (line 401) | render() {

FILE: src/panel/statistics/NodeStatistics.tsx
  type NodeStatisticsProps (line 7) | interface NodeStatisticsProps {
  function getStatisticsTable (line 20) | function getStatisticsTable(noDataText: string, nodeList: TableContent[]) {

FILE: src/panel/statistics/SortableTable.tsx
  type SortableTableProps (line 5) | interface SortableTableProps {
  function sort (line 10) | function sort(a: string, b: string, order: string, ignoreLiteral: string) {

FILE: src/panel/statistics/Statistics.tsx
  type StatisticsProps (line 8) | interface StatisticsProps {

FILE: src/panel/statistics/utils/Utils.ts
  function roundPercentageToDecimal (line 1) | function roundPercentageToDecimal(decimal: number, value: string) {
  function _getDecimalsOf (line 11) | function _getDecimalsOf(value: number) {

FILE: src/processing/graph_generator.ts
  class GraphGenerator (line 16) | class GraphGenerator {
    method constructor (line 20) | constructor(controller: PanelController) {
    method _createNode (line 25) | _createNode(dataElements: GraphDataElement[], nodeTree: NodeTree): Int...
    method _createMissingNodes (line 121) | _createMissingNodes(data: GraphDataElement[], nodes: IntGraphNode[]): ...
    method _createNodes (line 158) | _createNodes(data: GraphDataElement[]): IntGraphNode[] {
    method _resolveSubstitute (line 179) | _resolveSubstitute(name: string): string {
    method _createEdge (line 187) | _createEdge(dataElement: GraphDataElement): IntGraphEdge | undefined {
    method _resolveEdgeMap (line 234) | _resolveEdgeMap(edges: IntGraphEdge[]) {
    method _mergeArrayOfEdges (line 246) | _mergeArrayOfEdges(edges: IntGraphEdge[]) {
    method _edgeMapToMergedEdges (line 322) | _edgeMapToMergedEdges(edgeMap: Map<string, IntGraphEdge[]>) {
    method _mergeEdges (line 330) | _mergeEdges(edges: IntGraphEdge[]) {
    method _createEdges (line 337) | _createEdges(data: GraphDataElement[]): IntGraphEdge[] {
    method _filterData (line 349) | _filterData(graph: IntGraph): IntGraph {
    method generateGraph (line 392) | generateGraph(graphData: GraphDataElement[]): IntGraph {
    method _updateMaxLayer (line 405) | _updateMaxLayer(layer: number) {

FILE: src/processing/node_substitutor.ts
  class NodeSubstitutor (line 4) | class NodeSubstitutor {
    method constructor (line 7) | constructor() {
    method add (line 11) | add(node: IntGraphNode) {
    method substituteUntilLayer (line 20) | substituteUntilLayer(nodeName: string, layer: number, maxLayer: number) {

FILE: src/processing/node_tree.ts
  class NodeTree (line 4) | class NodeTree {
    method constructor (line 8) | constructor() {
    method addNode (line 13) | addNode(node: IntGraphNode) {
    method getNodesFromLayer (line 19) | getNodesFromLayer(layer: number) {
    method getNamePath (line 31) | getNamePath(namePath: string[]) {
    method _getNodesFromLayer (line 39) | private _getNodesFromLayer(currentNode: NodeTreeElement, layer: number...
    method _getNameSpaceFromCurrentLevel (line 56) | private _getNameSpaceFromCurrentLevel(namespace: string[], currentLeve...
    method _sumMetrics (line 64) | private _sumMetrics(sourceNode: IntGraphNode, targetNode: IntGraphNode...
    method _getObjectFromArray (line 119) | private _getObjectFromArray(array: NodeTreeElement[], id: string) {
    method _addNode (line 128) | private _addNode(nodeToAdd: NodeTreeElement, currentLayerNode: NodeTre...

FILE: src/processing/pre_processor.ts
  class PreProcessor (line 6) | class PreProcessor {
    method constructor (line 9) | constructor(controller: PanelController) {
    method _transformObjects (line 13) | _transformObjects(data: any[]): GraphDataElement[] {
    method _mergeGraphData (line 91) | _mergeGraphData(data: GraphDataElement[]): GraphDataElement[] {
    method _cleanMetaData (line 103) | _cleanMetaData(columnMapping: any, metaData: any) {
    method _extractColumnNames (line 115) | _extractColumnNames(data: GraphDataElement[]): string[] {
    method _getField (line 125) | _getField(fieldName: string, fields: any[]) {
    method _mergeSeries (line 134) | _mergeSeries(series: any[]) {
    method _dataToRows (line 153) | _dataToRows(inputDataSets: any) {
    method _resolveData (line 218) | _resolveData(row: any) {
    method _mergeObjects (line 269) | _mergeObjects(rows: any[]) {
    method processData (line 278) | processData(inputData: DataFrame[]): CurrentData {

FILE: src/processing/utils/Utils.ts
  function isPresent (line 5) | function isPresent<T>(t: T | undefined | null | void): t is T {

FILE: src/types.tsx
  type PanelSettings (line 3) | interface PanelSettings {
  type DataMapping (line 18) | interface DataMapping {
  type PanelStyleSettings (line 40) | interface PanelStyleSettings {
  type IconResource (line 46) | interface IconResource {
  type QueryResponseColumn (line 51) | interface QueryResponseColumn {
  type CyData (line 56) | interface CyData {
  type CurrentData (line 71) | interface CurrentData {
  type GraphDataElement (line 77) | interface GraphDataElement {
  type DataElement (line 85) | interface DataElement {
  type GraphDataType (line 96) | enum GraphDataType {
  type IntGraph (line 103) | interface IntGraph {
  type IntGraphNode (line 108) | interface IntGraphNode {
  type IntGraphNodeData (line 112) | interface IntGraphNodeData {
  type IntGraphMetrics (line 123) | interface IntGraphMetrics {
  type EnGraphNodeType (line 131) | enum EnGraphNodeType {
  type IntGraphEdge (line 137) | interface IntGraphEdge {
  type IntGraphEdgeData (line 144) | interface IntGraphEdgeData {
  type NodeTreeElement (line 150) | interface NodeTreeElement {
  type Particle (line 156) | interface Particle {
  type Particles (line 161) | interface Particles {
  type TableContent (line 166) | interface TableContent {
  type IntSelectionStatistics (line 173) | interface IntSelectionStatistics {
  type CyCanvas (line 181) | interface CyCanvas {
  type IntTableHeader (line 188) | interface IntTableHeader {
  type NodeData (line 200) | interface NodeData {
  type ScaleValue (line 207) | interface ScaleValue {
  type DrawContext (line 212) | interface DrawContext {
  type Rectangle (line 224) | interface Rectangle {
  type Point (line 230) | interface Point {
Condensed preview — 74 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (207K chars).
[
  {
    "path": ".circleci/config.yml",
    "chars": 1758,
    "preview": "version: 2.1\n\nparameters:\n  ssh-fingerprint:\n    type: string\n    default: ${GITHUB_SSH_FINGERPRINT}\n\naliases:\n  # Workf"
  },
  {
    "path": ".codeclimate.yml",
    "chars": 126,
    "preview": "exclude_patterns:\n- \"dist\"\n- \"**/node_modules/\"\n- \"**/*.d.ts\"\n- \"src/libs/\"\n- \"src/images/\"\n- \"src/img/\"\n- \"src/screensh"
  },
  {
    "path": ".config/.eslintrc",
    "chars": 407,
    "preview": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order"
  },
  {
    "path": ".config/.prettierrc.js",
    "chars": 385,
    "preview": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order"
  },
  {
    "path": ".config/Dockerfile",
    "chars": 725,
    "preview": "ARG grafana_version=latest\nARG grafana_image=grafana-enterprise\n\nFROM grafana/${grafana_image}:${grafana_version}\n\n# Mak"
  },
  {
    "path": ".config/README.md",
    "chars": 5092,
    "preview": "# Default build configuration by Grafana\n\n**This is an auto-generated directory and is not intended to be changed! ⚠️**\n"
  },
  {
    "path": ".config/jest/mocks/react-inlinesvg.tsx",
    "chars": 857,
    "preview": "// Due to the grafana/ui Icon component making fetch requests to\n// `/public/img/icon/<icon_name>.svg` we need to mock r"
  },
  {
    "path": ".config/jest/utils.js",
    "chars": 867,
    "preview": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order"
  },
  {
    "path": ".config/jest-setup.js",
    "chars": 848,
    "preview": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order"
  },
  {
    "path": ".config/jest.config.js",
    "chars": 1465,
    "preview": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order"
  },
  {
    "path": ".config/tsconfig.json",
    "chars": 741,
    "preview": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order"
  },
  {
    "path": ".config/types/custom.d.ts",
    "chars": 602,
    "preview": "// Image declarations\ndeclare module '*.gif' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.jpg' {\n  "
  },
  {
    "path": ".config/webpack/constants.ts",
    "chars": 65,
    "preview": "export const SOURCE_DIR = 'src';\nexport const DIST_DIR = 'dist';\n"
  },
  {
    "path": ".config/webpack/utils.ts",
    "chars": 1710,
    "preview": "import fs from 'fs';\nimport process from 'process';\nimport os from 'os';\nimport path from 'path';\nimport util from 'util"
  },
  {
    "path": ".config/webpack/webpack.config.ts",
    "chars": 6477,
    "preview": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order"
  },
  {
    "path": ".editorconfig",
    "chars": 261,
    "preview": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace"
  },
  {
    "path": ".eslintrc",
    "chars": 38,
    "preview": "{\n  \"extends\": \"./.config/.eslintrc\"\n}"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 803,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[BUG] - *ENTER TITLE*\"\nlabels: bug\nassignees: ''\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 629,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[Feature] - *ENTER TITLE*\"\nlabels: enhancement"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.md",
    "chars": 227,
    "preview": "---\nname: Question\nabout: Ask a question about the project.\ntitle: \"[Question] - *ENTER TITLE*\"\nlabels: question\nassigne"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 261,
    "preview": "name: Build plugin\n\non:\n  push:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    container:\n      image: node:20.19\n    st"
  },
  {
    "path": ".gitignore",
    "chars": 54,
    "preview": "node_modules\nnpm-debug.log\n*.log\n.vscode\ndist\ncoverage"
  },
  {
    "path": ".nvmrc",
    "chars": 2,
    "preview": "16"
  },
  {
    "path": ".prettierrc.js",
    "chars": 122,
    "preview": "module.exports = {\n  // Prettier configuration provided by Grafana scaffolding\n  ...require(\"./.config/.prettierrc.js\")\n"
  },
  {
    "path": ".release-it.json",
    "chars": 333,
    "preview": "{\n    \"github\": {\n      \"release\": false\n    },\n    \"npm\": {\n      \"publish\": false\n    },\n    \"hooks\": {\n      \"after:b"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 760,
    "preview": "# 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## v"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1441,
    "preview": "# Contributing\n\n## Releasing\n\nTo create a new release of the plugin, follow these steps.\n\nYou may also read the [officia"
  },
  {
    "path": "LICENSE",
    "chars": 10768,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 10040,
    "preview": "## Novatec Service Dependency Graph Panel\n\n![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%"
  },
  {
    "path": "appveyor.yml",
    "chars": 622,
    "preview": "# Test against the latest version of this Node.js version\nenvironment:\n  nodejs_version: \"12\"\n\n# Local NPM Modules\ncache"
  },
  {
    "path": "docker-compose.yaml",
    "chars": 562,
    "preview": "services:\n  grafana:\n    container_name: 'novatec-sdg-panel'\n    platform: \"linux/amd64\"\n    build:\n      context: ./.co"
  },
  {
    "path": "docs/README.md",
    "chars": 33,
    "preview": "TODO: add example docs structure\n"
  },
  {
    "path": "jest-setup.js",
    "chars": 77,
    "preview": "// Jest setup provided by Grafana scaffolding\nimport './.config/jest-setup';\n"
  },
  {
    "path": "jest.config.js",
    "chars": 281,
    "preview": "// force timezone to UTC to allow tests to work regardless of local timezone\n// generally used by snapshots, but can aff"
  },
  {
    "path": "package.json",
    "chars": 3257,
    "preview": "{\n  \"name\": \"novatec-service-dependency-graph-panel\",\n  \"version\": \"4.2.0\",\n  \"description\": \"Service Dependency Graph p"
  },
  {
    "path": "provisioning/dashboards/dashboards.yml",
    "chars": 284,
    "preview": "apiVersion: 1\n\nproviders:\n  - name: 'default'\n    orgId: 1\n    folder: ''\n    type: file\n    disableDeletion: true\n    a"
  },
  {
    "path": "provisioning/home/home.json",
    "chars": 3134,
    "preview": "{\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"grafana\",\n  "
  },
  {
    "path": "scripts/create-signed-plugin.sh",
    "chars": 253,
    "preview": "#!/bin/bash\n\necho Building plugin...\nyarn install && yarn build\n\necho Signing plugin...\nyarn sign\n\necho Creating zip fil"
  },
  {
    "path": "src/assets/icons/icon_index.json",
    "chars": 119,
    "preview": "[\"java\", \"star_trek\", \"balancer\", \"database\", \"default\", \"ftp\", \"http\", \"ldap\", \"mainframe\", \"message\", \"smtp\", \"web\"]\n"
  },
  {
    "path": "src/css/novatec-service-dependency-graph-panel.css",
    "chars": 2202,
    "preview": ".service-dependency-graph-panel {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-direction: c"
  },
  {
    "path": "src/dummy_data_frame.ts",
    "chars": 13786,
    "preview": "import { ArrayVector, DataFrame, FieldType } from '@grafana/data';\n\nconst data: DataFrame[] = [\n  {\n    refId: 'A',\n    "
  },
  {
    "path": "src/migration/PanelMigration.tsx",
    "chars": 3045,
    "preview": "import { PanelModel } from '@grafana/data';\nimport { DefaultSettings } from 'options/DefaultSettings';\nimport { PanelSet"
  },
  {
    "path": "src/module.test.ts",
    "chars": 133,
    "preview": "// Just a stub test\ndescribe('placeholder test', () => {\n  it('should return true', () => {\n    expect(true).toBeTruthy("
  },
  {
    "path": "src/module.ts",
    "chars": 419,
    "preview": "import { PanelPlugin } from '@grafana/data';\nimport { PanelSettings } from './types';\n\nimport { PanelController } from '"
  },
  {
    "path": "src/options/DefaultSettings.tsx",
    "chars": 1448,
    "preview": "import { PanelSettings } from '../types';\n\nexport const DefaultSettings: PanelSettings = {\n  animate: true,\n\n  dataMappi"
  },
  {
    "path": "src/options/TypeAheadTextfield/TypeaheadTextfield.css",
    "chars": 352,
    "preview": ".service-dependency-graph-panel .suggestion {\n    width: 100%;\n    border-right: 1px solid #3865AB ;\n    border-left: 1p"
  },
  {
    "path": "src/options/TypeAheadTextfield/TypeaheadTextfield.tsx",
    "chars": 3407,
    "preview": "import React from 'react';\nimport Autosuggest, { InputProps } from 'react-autosuggest';\nimport { StandardEditorContext, "
  },
  {
    "path": "src/options/dummyDataSwitch/DummyDataSwitch.tsx",
    "chars": 2272,
    "preview": "import React from 'react';\nimport { StandardEditorContext, StandardEditorProps } from '@grafana/data';\nimport { PanelSet"
  },
  {
    "path": "src/options/iconMapping/IconMapping.css",
    "chars": 416,
    "preview": ".service-dependency-graph-panel .no-background {\n    background-color: transparent;\n}\n\n.service-dependency-graph-panel ."
  },
  {
    "path": "src/options/iconMapping/IconMapping.tsx",
    "chars": 4200,
    "preview": "import React, { ChangeEvent } from 'react';\nimport { StandardEditorContext, StandardEditorProps } from '@grafana/data';\n"
  },
  {
    "path": "src/options/options.tsx",
    "chars": 9258,
    "preview": "import { PanelOptionsEditorBuilder } from '@grafana/data';\nimport { PanelSettings } from '../types';\nimport { TypeaheadT"
  },
  {
    "path": "src/panel/PanelController.tsx",
    "chars": 6804,
    "preview": "import React, { LegacyRef, PureComponent } from 'react';\nimport {\n  AbsoluteTimeRange,\n  DataFrame,\n  FieldConfigSource,"
  },
  {
    "path": "src/panel/asset_utils.tsx",
    "chars": 712,
    "preview": "import { find } from 'lodash';\nimport { IconResource } from 'types';\n\nexport default {\n  getAssetUrl(assetName: string) "
  },
  {
    "path": "src/panel/canvas/collision_detector.ts",
    "chars": 1425,
    "preview": "import _ from 'lodash';\nimport { Point, Rectangle } from 'types';\n\nexport default class CollisionDetector {\n  blockedAre"
  },
  {
    "path": "src/panel/canvas/graph_canvas.ts",
    "chars": 24089,
    "preview": "import _ from 'lodash';\nimport cytoscape from 'cytoscape';\nimport { ServiceDependencyGraph } from '../serviceDependencyG"
  },
  {
    "path": "src/panel/canvas/particle_engine.ts",
    "chars": 2813,
    "preview": "import CanvasDrawer from './graph_canvas';\nimport _ from 'lodash';\nimport { Particles, Particle, IntGraphMetrics } from "
  },
  {
    "path": "src/panel/layout_options.ts",
    "chars": 2556,
    "preview": "const options = {\n  name: 'cola',\n  animate: true, // whether to show the layout as it's running\n  refresh: 1, // number"
  },
  {
    "path": "src/panel/serviceDependencyGraph/ServiceDependencyGraph.css",
    "chars": 374,
    "preview": ".service-dependency-graph-panel .graph-container {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    flex-direct"
  },
  {
    "path": "src/panel/serviceDependencyGraph/ServiceDependencyGraph.tsx",
    "chars": 12062,
    "preview": "import CanvasDrawer from 'panel/canvas/graph_canvas';\nimport cytoscape, { EdgeCollection, EdgeSingular, ElementDefinitio"
  },
  {
    "path": "src/panel/statistics/IncomingStatistics.tsx",
    "chars": 1329,
    "preview": "import React from 'react';\nimport { TableContent } from 'types';\n\nexport const NodeStatistics = (receiving: TableContent"
  },
  {
    "path": "src/panel/statistics/NodeStatistics.tsx",
    "chars": 1443,
    "preview": "import React from 'react';\nimport { IntTableHeader } from '../../types';\nimport { TableContent } from 'types';\nimport So"
  },
  {
    "path": "src/panel/statistics/SortableTable.tsx",
    "chars": 1328,
    "preview": "import React from 'react';\nimport { IntTableHeader, NodeData } from '../../types';\nimport BootstrapTable from 'react-boo"
  },
  {
    "path": "src/panel/statistics/Statistics.css",
    "chars": 297,
    "preview": ".service-dependency-graph-panel .margin {\n    margin: 10px;\n}\n\n.service-dependency-graph-panel .statistics {\n    flex-ba"
  },
  {
    "path": "src/panel/statistics/Statistics.tsx",
    "chars": 3879,
    "preview": "import React from 'react';\nimport { NodeStatistics } from './NodeStatistics';\nimport '../../css/novatec-service-dependen"
  },
  {
    "path": "src/panel/statistics/utils/Utils.ts",
    "chars": 463,
    "preview": "function roundPercentageToDecimal(decimal: number, value: string) {\n  if (value !== '-') {\n    let valueDecimals = _getD"
  },
  {
    "path": "src/plugin.json",
    "chars": 1471,
    "preview": "{\n  \"type\": \"panel\",\n  \"name\": \"Service Dependency Graph\",\n  \"id\": \"novatec-sdg-panel\",\n  \"info\": {\n    \"version\": \"4.2."
  },
  {
    "path": "src/processing/graph_generator.ts",
    "chars": 13386,
    "preview": "import _ from 'lodash';\nimport { isPresent } from './utils/Utils';\nimport { PanelController } from '../panel/PanelContro"
  },
  {
    "path": "src/processing/node_substitutor.ts",
    "chars": 825,
    "preview": "import _ from 'lodash';\nimport { IntGraphNode } from '../types';\n\nclass NodeSubstitutor {\n  private _substitutionMap: an"
  },
  {
    "path": "src/processing/node_tree.ts",
    "chars": 6813,
    "preview": "import _ from 'lodash';\nimport { EnGraphNodeType, IntGraphMetrics, IntGraphNode, NodeTreeElement } from '../types';\n\ncla"
  },
  {
    "path": "src/processing/pre_processor.ts",
    "chars": 10093,
    "preview": "import { DataFrame } from '@grafana/data';\nimport _ from 'lodash';\nimport { PanelController } from '../panel/PanelContro"
  },
  {
    "path": "src/processing/utils/Utils.ts",
    "chars": 448,
    "preview": "import _ from 'lodash';\nimport { DataMapping } from '../../types';\nimport { ServiceDependencyGraph } from 'panel/service"
  },
  {
    "path": "src/types.tsx",
    "chars": 4485,
    "preview": "import { DataFrame } from '@grafana/data';\n\nexport interface PanelSettings {\n  animate: boolean;\n  sumTimings: boolean;\n"
  },
  {
    "path": "src/typings/index.d.ts",
    "chars": 180,
    "preview": "declare module 'cytoscape-canvas';\ndeclare module 'cytoscape-cola';\ndeclare module 'human-format';\ndeclare module 'react"
  },
  {
    "path": "tsconfig.json",
    "chars": 273,
    "preview": "{\n  \"extends\": \"./.config/tsconfig.json\",\n  \"include\": [\"src\", \"types\"],\n  \"jsx\": \"react\",\n  \"compilerOptions\": {\n    \"s"
  }
]

About this extraction

This page contains the full source code of the NovatecConsulting/novatec-service-dependency-graph-panel GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 74 files (190.4 KB), approximately 49.2k tokens, and a symbol index with 197 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!