Bläddra i källkod

Revamp the entire source code style and formatting

Changes all the source code coding style and code formatting using the
latest `prettier` and `eslint` library versions.

By doing this we enforce a stricter coding style and we also apply code
formatting, previously all the formatting was done by the `eslint`
library which, expectedly, didn't do such a great job (its main job is
coding rules not actual code formatting).
Sergiu Miclea 3 år sedan
förälder
incheckning
3dc8fa6372
100 ändrade filer med 4681 tillägg och 3973 borttagningar
  1. 0 0
      .eslintignore
  2. 0 98
      .eslintrc
  3. 38 0
      .eslintrc.js
  4. 3 0
      .prettierrc
  5. 0 16
      .vscode/settings.json
  6. 10 11
      package.json
  7. 66 52
      server/api/ConfigApi.ts
  8. 76 58
      server/api/LogosApi.ts
  9. 13 13
      server/api/router.ts
  10. 4 3
      server/index.js
  11. 15 15
      server/main.ts
  12. 59 39
      server/proxies/azureProxy.ts
  13. 13 11
      server/proxies/metalHubProxy.ts
  14. 9 9
      server/proxies/router.ts
  15. 47 47
      src/@types/Assessment.ts
  16. 4 4
      src/@types/Cache.ts
  17. 38 38
      src/@types/Config.ts
  18. 57 48
      src/@types/Endpoint.ts
  19. 15 11
      src/@types/Execution.ts
  20. 80 75
      src/@types/Field.ts
  21. 15 14
      src/@types/InitialSetup.ts
  22. 36 36
      src/@types/Instance.ts
  23. 17 17
      src/@types/Licence.ts
  24. 2 2
      src/@types/Log.ts
  25. 93 84
      src/@types/MainItem.ts
  26. 34 34
      src/@types/MetalHub.ts
  27. 43 41
      src/@types/MinionPool.ts
  28. 29 24
      src/@types/Network.ts
  29. 22 22
      src/@types/NotificationItem.ts
  30. 19 19
      src/@types/Project.ts
  31. 17 4
      src/@types/Providers.ts
  32. 6 6
      src/@types/Region.ts
  33. 16 16
      src/@types/Schedule.ts
  34. 24 21
      src/@types/Schema.ts
  35. 16 16
      src/@types/Task.ts
  36. 19 19
      src/@types/User.ts
  37. 15 15
      src/@types/WizardData.ts
  38. 10 10
      src/@types/declarations.d.ts
  39. 146 113
      src/components/App.tsx
  40. 47 39
      src/components/Theme.ts
  41. 52 41
      src/components/modules/AssessmentModule/AssessedVmListItem/AssessedVmListItem.tsx
  42. 1 2
      src/components/modules/AssessmentModule/AssessedVmListItem/package.json
  43. 286 186
      src/components/modules/AssessmentModule/AssessmentDetailsContent/AssessmentDetailsContent.tsx
  44. 1 2
      src/components/modules/AssessmentModule/AssessmentDetailsContent/package.json
  45. 39 43
      src/components/modules/AssessmentModule/AssessmentListItem/AssessmentListItem.tsx
  46. 1 2
      src/components/modules/AssessmentModule/AssessmentListItem/package.json
  47. 136 117
      src/components/modules/AssessmentModule/AssessmentMigrationOptions/AssessmentMigrationOptions.tsx
  48. 1 2
      src/components/modules/AssessmentModule/AssessmentMigrationOptions/package.json
  49. 80 63
      src/components/modules/DashboardModule/DashboardActivity/DashboardActivity.spec.tsx
  50. 62 49
      src/components/modules/DashboardModule/DashboardActivity/DashboardActivity.tsx
  51. 39 34
      src/components/modules/DashboardModule/DashboardBarChart/BarChartNiceScale.ts
  52. 73 46
      src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.spec.tsx
  53. 93 69
      src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.tsx
  54. 86 78
      src/components/modules/DashboardModule/DashboardContent/DashboardContent.tsx
  55. 0 1
      src/components/modules/DashboardModule/DashboardContent/package.json
  56. 128 104
      src/components/modules/DashboardModule/DashboardExecutions/DashboardExecutions.tsx
  57. 30 25
      src/components/modules/DashboardModule/DashboardInfoCount/DashboardInfoCount.tsx
  58. 101 80
      src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.tsx
  59. 112 101
      src/components/modules/DashboardModule/DashboardPieChart/DashboardPieChart.tsx
  60. 137 94
      src/components/modules/DashboardModule/DashboardTopEndpoints/DashboardTopEndpoints.tsx
  61. 44 49
      src/components/modules/DetailsModule/DetailsContentHeader/DetailsContentHeader.tsx
  62. 1 2
      src/components/modules/DetailsModule/DetailsContentHeader/package.json
  63. 22 36
      src/components/modules/DetailsModule/DetailsContentHeader/story.tsx
  64. 52 53
      src/components/modules/DetailsModule/DetailsContentHeader/test.tsx
  65. 57 46
      src/components/modules/DetailsModule/DetailsPageHeader/DetailsPageHeader.tsx
  66. 1 2
      src/components/modules/DetailsModule/DetailsPageHeader/package.json
  67. 40 41
      src/components/modules/DetailsModule/DetailsPageHeader/test.tsx
  68. 246 178
      src/components/modules/EndpointModule/ChooseProvider/ChooseProvider.tsx
  69. 108 105
      src/components/modules/EndpointModule/ChooseProvider/MultipleUploadedEndpoints.tsx
  70. 1 2
      src/components/modules/EndpointModule/ChooseProvider/package.json
  71. 15 16
      src/components/modules/EndpointModule/ChooseProvider/story.tsx
  72. 40 38
      src/components/modules/EndpointModule/ChooseProvider/test.tsx
  73. 129 97
      src/components/modules/EndpointModule/EndpointDetailsContent/EndpointDetailsContent.tsx
  74. 1 2
      src/components/modules/EndpointModule/EndpointDetailsContent/package.json
  75. 21 17
      src/components/modules/EndpointModule/EndpointDetailsContent/story.tsx
  76. 98 79
      src/components/modules/EndpointModule/EndpointDetailsContent/test.tsx
  77. 56 43
      src/components/modules/EndpointModule/EndpointDuplicateOptions/EndpointDuplicateOptions.tsx
  78. 1 2
      src/components/modules/EndpointModule/EndpointDuplicateOptions/package.json
  79. 15 15
      src/components/modules/EndpointModule/EndpointDuplicateOptions/story.tsx
  80. 48 48
      src/components/modules/EndpointModule/EndpointDuplicateOptions/test.tsx
  81. 38 32
      src/components/modules/EndpointModule/EndpointListItem/EndpointListItem.tsx
  82. 1 2
      src/components/modules/EndpointModule/EndpointListItem/package.json
  83. 37 31
      src/components/modules/EndpointModule/EndpointListItem/story.tsx
  84. 43 38
      src/components/modules/EndpointModule/EndpointListItem/test.tsx
  85. 44 36
      src/components/modules/EndpointModule/EndpointLogos/EndpointLogos.tsx
  86. 1 2
      src/components/modules/EndpointModule/EndpointLogos/package.json
  87. 53 44
      src/components/modules/EndpointModule/EndpointLogos/resources/Generic.tsx
  88. 47 54
      src/components/modules/EndpointModule/EndpointLogos/story.tsx
  89. 28 30
      src/components/modules/EndpointModule/EndpointLogos/test.tsx
  90. 253 200
      src/components/modules/EndpointModule/EndpointModal/EndpointModal.tsx
  91. 71 47
      src/components/modules/EndpointModule/EndpointValidation/EndpointValidation.tsx
  92. 1 2
      src/components/modules/EndpointModule/EndpointValidation/package.json
  93. 25 14
      src/components/modules/EndpointModule/EndpointValidation/story.tsx
  94. 39 40
      src/components/modules/EndpointModule/EndpointValidation/test.tsx
  95. 237 170
      src/components/modules/LicenceModule/LicenceModule.tsx
  96. 1 1
      src/components/modules/LicenceModule/images/licence.ts
  97. 85 68
      src/components/modules/LoginModule/LoginForm/LoginForm.tsx
  98. 1 2
      src/components/modules/LoginModule/LoginForm/package.json
  99. 9 15
      src/components/modules/LoginModule/LoginForm/story.tsx
  100. 40 35
      src/components/modules/LoginModule/LoginForm/test.tsx

+ 0 - 0
.eslintignore


+ 0 - 98
.eslintrc

@@ -1,98 +0,0 @@
-{
-  "extends": [
-    "airbnb",
-    "airbnb-typescript"
-  ],
-  "parser": "@typescript-eslint/parser",
-  "overrides": [
-    {
-      "files": [
-        "*.ts",
-        "*.tsx"
-      ],
-      "parserOptions": {
-        "project": "./tsconfig.json"
-      }
-    }
-  ],
-  "plugins": [
-    "import",
-    "react",
-    "coriolis-web"
-  ],
-  "env": {
-    "browser": true,
-    "node": true
-  },
-  "globals": {},
-  "ignorePatterns": [
-    "*.svg",
-    "*.png",
-    "*.bash",
-    "*.log",
-    "*.jpg",
-    "*.woff",
-    "*.js",
-    "*.snap",
-    "src/**/test.tsx",
-    "src/**/package.json"
-  ],
-  "rules": {
-    "max-params": [
-      "error",
-      3
-    ],
-    "coriolis-web/import-no-duplicate-name": "error",
-    "react/function-component-definition": "off",
-    "react/sort-comp": "off",
-    "react/jsx-one-expression-per-line": "off",
-    "@typescript-eslint/semi": [
-      2,
-      "never"
-    ],
-    "arrow-parens": [
-      2,
-      "as-needed"
-    ],
-    "no-console": "off",
-    "react/require-default-props": "off",
-    "class-methods-use-this": "off",
-    "no-underscore-dangle": "off",
-    "jsx-a11y/mouse-events-have-key-events": "off",
-    "react/jsx-no-duplicate-props": "off",
-    "no-nested-ternary": "off",
-    "no-dupe-class-members": "off",
-    "object-curly-spacing": [
-      "error",
-      "always"
-    ],
-    "no-throw-literal": "off",
-    "@typescript-eslint/type-annotation-spacing": [
-      "error",
-      {
-        "after": true
-      }
-    ],
-    "react/state-in-constructor": "off",
-    "react/destructuring-assignment": "off",
-    "import/no-extraneous-dependencies": "off",
-    "react/static-property-placement": "off",
-    "react/no-danger": "off",
-    "prefer-destructuring": "off",
-    "import/no-cycle": "off",
-    "@typescript-eslint/camelcase": "off",
-    "react/jsx-props-no-spreading": "off",
-    "max-classes-per-file": "off",
-    "prefer-promise-reject-errors": "off",
-    "import/prefer-default-export": "off",
-    "no-param-reassign": "off",
-    "max-len": [
-      "error",
-      {
-        "code": 150,
-        "ignoreTemplateLiterals": true,
-        "ignoreStrings": true
-      }
-    ]
-  }
-}

+ 38 - 0
.eslintrc.js

@@ -0,0 +1,38 @@
+module.exports = {
+  env: {
+    browser: true,
+    es2021: true,
+    jest: true,
+    node: true,
+  },
+  extends: [
+    "eslint:recommended",
+    "plugin:react/recommended",
+    "plugin:@typescript-eslint/recommended",
+    "prettier",
+  ],
+  overrides: [],
+  parser: "@typescript-eslint/parser",
+  parserOptions: {
+    ecmaVersion: "latest",
+    sourceType: "module",
+  },
+  plugins: ["react", "@typescript-eslint", "prettier", "coriolis-web"],
+  settings: {
+    "import/resolver": {
+      typescript: {},
+    },
+    react: {
+      version: "detect",
+    },
+  },
+  ignorePatterns: ["*.svg", "*.png", "*.jpg", "*.jpeg", "*.woff"],
+  rules: {
+    "coriolis-web/import-no-duplicate-name": "error",
+    "@typescript-eslint/ban-ts-comment": "off",
+    "@typescript-eslint/no-explicit-any": "off",
+    "@typescript-eslint/no-non-null-assertion": "off",
+    "@typescript-eslint/no-empty-function": "off",
+    "@typescript-eslint/no-non-null-asserted-optional-chain": "off",
+  },
+};

+ 3 - 0
.prettierrc

@@ -0,0 +1,3 @@
+{
+  "arrowParens": "avoid"
+}

+ 0 - 16
.vscode/settings.json

@@ -1,20 +1,4 @@
 {
-  "editor.formatOnSave": true,
-  "[javascript]": {
-    "editor.formatOnSave": false,
-  },
-  "[javascriptreact]": {
-    "editor.formatOnSave": false,
-  },
-  "[typescript]": {
-    "editor.formatOnSave": false,
-  },
-  "[typescriptreact]": {
-    "editor.formatOnSave": false,
-  },
-  "editor.codeActionsOnSave": {
-    "source.fixAll.eslint": true
-  },
   "files.eol": "\n",
   "typescript.tsdk": "node_modules/typescript/lib",
   "typescript.preferences.importModuleSpecifier": "non-relative",

+ 10 - 11
package.json

@@ -12,7 +12,8 @@
     "server-dev": "nodemon -e ts,js -w server/**/ -w .env",
     "server-debug": "node --inspect server",
     "tsc": "npx tsc --skipLibCheck",
-    "eslint": "npx eslint \"src/**\" \"server/**\"",
+    "eslint": "npx eslint \"src/**\" \"server/**\" --fix",
+    "format": "prettier --write src/**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc",
     "test": "jest",
     "test-release": "node ./tests/testRelease",
     "test-coverage": "node ./tests/testCoverage",
@@ -36,20 +37,18 @@
     "@types/react-router-dom": "^5.1.2",
     "@types/react-tooltip": "^4.2.4",
     "@types/styled-components": "^5.1.0",
-    "@typescript-eslint/eslint-plugin": "^5.6.0",
-    "@typescript-eslint/parser": "^5.6.0",
+    "@typescript-eslint/eslint-plugin": "^5.36.2",
+    "@typescript-eslint/parser": "^5.36.2",
     "cross-spawn": "^7.0.3",
     "cypress": "^3.2.0",
-    "eslint": "^8.2.0",
-    "eslint-config-airbnb": "19.0.0",
-    "eslint-config-airbnb-typescript": "^16.1.0",
+    "eslint": "^8.23.0",
     "eslint-plugin-coriolis-web": "./src/utils/eslint-plugin-coriolis-web",
-    "eslint-plugin-import": "^2.25.3",
-    "eslint-plugin-jsx-a11y": "^6.5.1",
-    "eslint-plugin-react": "^7.27.0",
-    "eslint-plugin-react-hooks": "^4.3.0",
+    "eslint-config-prettier": "^8.5.0",
+    "eslint-plugin-prettier": "^4.2.1",
+    "eslint-plugin-react": "^7.31.7",
     "jest": "^27.3.1",
-    "nodemon": "^2.0.4"
+    "nodemon": "^2.0.4",
+    "prettier": "^2.7.1"
   },
   "dependencies": {
     "@babel/core": "^7.7.2",

+ 66 - 52
server/api/ConfigApi.ts

@@ -12,64 +12,78 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import express from 'express'
-import path from 'path'
-import fs from 'fs'
-import requireWithoutCache from 'require-without-cache'
+import express from "express";
+import path from "path";
+import fs from "fs";
+import requireWithoutCache from "require-without-cache";
 
-import type { Services } from '@src/@types/Config'
+import type { Services } from "@src/@types/Config";
 
 const getBaseUrl = () => {
-  const BASE_URL = process.env.CORIOLIS_URL || ''
-  return BASE_URL.trim().replace(/\/$/, '')
-}
+  const BASE_URL = process.env.CORIOLIS_URL || "";
+  return BASE_URL.trim().replace(/\/$/, "");
+};
 
-const modServicesUrls = (configServices: Services, servicesMod?: Services): Services => {
-  const services = { ...configServices }
+const modServicesUrls = (
+  configServices: Services,
+  servicesMod?: Services
+): Services => {
+  const services = { ...configServices };
 
-  Object.keys(services).forEach(key => {
-    const typedKey = key as keyof Services
+  Object.keys(services).forEach((key) => {
+    const typedKey = key as keyof Services;
     services[typedKey] = (
-      servicesMod && servicesMod[typedKey] ? servicesMod[typedKey] : services[typedKey]
-    ).replace('{BASE_URL}', getBaseUrl())
-  })
-  return services
-}
+      servicesMod && servicesMod[typedKey]
+        ? servicesMod[typedKey]
+        : services[typedKey]
+    ).replace("{BASE_URL}", getBaseUrl());
+  });
+  return services;
+};
 
-const NOT_FIRST_LAUNCH_PATH = path.join(__dirname, '../../.not-first-launch')
+const NOT_FIRST_LAUNCH_PATH = path.join(__dirname, "../../.not-first-launch");
 
 export default (router: express.Router) => {
-  router.get('/config', (_, res) => {
-    const configPath = path.join(__dirname, '../../config.ts')
-    const isFirstLaunch = !fs.existsSync(NOT_FIRST_LAUNCH_PATH)
-    const config: any = requireWithoutCache(configPath, require).config
-    const modJsonPath: string | null | undefined = process.env.MOD_JSON
-    if (!modJsonPath) {
-      config.servicesUrls = modServicesUrls(config.servicesUrls)
-      res.send({ config, isFirstLaunch })
-      return
-    }
-    try {
-      const jsonContent: any = fs.readFileSync(modJsonPath)
-      const configMod = JSON.parse(jsonContent).config
-      Object.keys(configMod).forEach(key => {
-        if (key !== 'servicesUrls') {
-          config[key] = configMod[key]
-        }
-      })
-      config.servicesUrls = modServicesUrls(config.servicesUrls, configMod.servicesUrls)
-      res.send({ config, isFirstLaunch })
-    } catch (err) {
-      console.error(err)
-      res.status(400).json({ error: { message: 'Invalid MOD_JSON file' } })
-    }
-  }).post('/config/first-launch', (req, res) => {
-    const { isFirstLaunch } = req.body
-    if (isFirstLaunch !== false) {
-      res.status(422).json({ error: { message: '\'isFirstLaunch\' property not set to \'false\'' } })
-      return
-    }
-    fs.writeFileSync(NOT_FIRST_LAUNCH_PATH, '')
-    res.json({ isFirstLaunch })
-  })
-}
+  router
+    .get("/config", (_, res) => {
+      const configPath = path.join(__dirname, "../../config.ts");
+      const isFirstLaunch = !fs.existsSync(NOT_FIRST_LAUNCH_PATH);
+      const config: any = requireWithoutCache(configPath, require).config;
+      const modJsonPath: string | null | undefined = process.env.MOD_JSON;
+      if (!modJsonPath) {
+        config.servicesUrls = modServicesUrls(config.servicesUrls);
+        res.send({ config, isFirstLaunch });
+        return;
+      }
+      try {
+        const jsonContent: any = fs.readFileSync(modJsonPath);
+        const configMod = JSON.parse(jsonContent).config;
+        Object.keys(configMod).forEach((key) => {
+          if (key !== "servicesUrls") {
+            config[key] = configMod[key];
+          }
+        });
+        config.servicesUrls = modServicesUrls(
+          config.servicesUrls,
+          configMod.servicesUrls
+        );
+        res.send({ config, isFirstLaunch });
+      } catch (err) {
+        console.error(err);
+        res.status(400).json({ error: { message: "Invalid MOD_JSON file" } });
+      }
+    })
+    .post("/config/first-launch", (req, res) => {
+      const { isFirstLaunch } = req.body;
+      if (isFirstLaunch !== false) {
+        res
+          .status(422)
+          .json({
+            error: { message: "'isFirstLaunch' property not set to 'false'" },
+          });
+        return;
+      }
+      fs.writeFileSync(NOT_FIRST_LAUNCH_PATH, "");
+      res.json({ isFirstLaunch });
+    });
+};

+ 76 - 58
server/api/LogosApi.ts

@@ -12,98 +12,116 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import express from 'express'
-import path from 'path'
-import fs from 'fs'
+import express from "express";
+import path from "path";
+import fs from "fs";
 
 const getModJsonProviders = (jsonPath: string) => {
-  const jsonContent: any = fs.readFileSync(jsonPath)
-  const json = JSON.parse(jsonContent)
+  const jsonContent: any = fs.readFileSync(jsonPath);
+  const json = JSON.parse(jsonContent);
   if (!json.providers) {
-    throw new Error()
+    throw new Error();
   }
-  return json.providers
-}
+  return json.providers;
+};
 
 const getOptimalLogoHeightKey = (
   availableHeightKeys: string[],
   requestedHeight: number,
-  style?: string | null,
+  style?: string | null
 ): string => {
-  let heightKeys = availableHeightKeys
+  let heightKeys = availableHeightKeys;
   if (style) {
-    const styledKeys = heightKeys.filter(k => (style ? k.indexOf(style) > -1 : false))
+    const styledKeys = heightKeys.filter((k) =>
+      style ? k.indexOf(style) > -1 : false
+    );
     if (styledKeys.length) {
-      heightKeys = styledKeys
+      heightKeys = styledKeys;
     }
   }
 
   const optimal = heightKeys.reduce((prev, curr) => {
-    let prevHeight: any = /d+/.exec(prev)
-    let currHeight: any = /d+/.exec(curr)
-    prevHeight = prevHeight ? Number(prevHeight[0]) : 0
-    currHeight = currHeight ? Number(currHeight[0]) : 0
-    return Math.abs(currHeight - requestedHeight)
-      < Math.abs(prevHeight - requestedHeight) ? curr : prev
-  })
-  return optimal
-}
+    let prevHeight: any = /d+/.exec(prev);
+    let currHeight: any = /d+/.exec(curr);
+    prevHeight = prevHeight ? Number(prevHeight[0]) : 0;
+    currHeight = currHeight ? Number(currHeight[0]) : 0;
+    return Math.abs(currHeight - requestedHeight) <
+      Math.abs(prevHeight - requestedHeight)
+      ? curr
+      : prev;
+  });
+  return optimal;
+};
 
 export default (router: express.Router) => {
-  router.get('/logos/:provider/:size/:style?', (req, res) => {
-    const SIZES = [32, 42, 64, 128]
-    const STYLES = ['white', 'disabled']
-    const { provider, style } = req.params
-    const size = Number(req.params.size)
+  router.get("/logos/:provider/:size/:style?", (req, res) => {
+    const SIZES = [32, 42, 64, 128];
+    const STYLES = ["white", "disabled"];
+    const { provider, style } = req.params;
+    const size = Number(req.params.size);
 
     if (SIZES.indexOf(size) === -1) {
-      res.status(400).json({ error: { message: `Valid sizes are: ${SIZES.join(', ')}` } })
-      return
+      res
+        .status(400)
+        .json({ error: { message: `Valid sizes are: ${SIZES.join(", ")}` } });
+      return;
     }
     if (style && STYLES.indexOf(style) === -1) {
-      res.status(400).json({ error: { message: `Valid styles are: ${STYLES.join(', ')}` } })
-      return
+      res
+        .status(400)
+        .json({ error: { message: `Valid styles are: ${STYLES.join(", ")}` } });
+      return;
     }
-    const logoBase = path.join(__dirname, '/resources/providerLogos')
-    let logoPath = `${logoBase}/${provider}-${size}`
-    logoPath = style ? `${logoPath}-${style}.svg` : `${logoPath}.svg`
+    const logoBase = path.join(__dirname, "/resources/providerLogos");
+    let logoPath = `${logoBase}/${provider}-${size}`;
+    logoPath = style ? `${logoPath}-${style}.svg` : `${logoPath}.svg`;
 
-    const modJsonPath: string | null | undefined = process.env.MOD_JSON
+    const modJsonPath: string | null | undefined = process.env.MOD_JSON;
     if (!modJsonPath) {
-      res.sendFile(logoPath)
-      return
+      res.sendFile(logoPath);
+      return;
     }
 
     try {
-      const providersJson = getModJsonProviders(modJsonPath)
-      const providerJson = providersJson[provider]
+      const providersJson = getModJsonProviders(modJsonPath);
+      const providerJson = providersJson[provider];
       if (!providerJson) {
-        res.sendFile(logoPath)
-        return
+        res.sendFile(logoPath);
+        return;
       }
-      const providerLogosJson = providerJson.logos
+      const providerLogosJson = providerJson.logos;
       if (!providerLogosJson) {
-        console.log(`No logos specified in MOD_JSON file for '${provider}' provider`)
-        res.sendFile(logoPath)
-        return
+        console.log(
+          `No logos specified in MOD_JSON file for '${provider}' provider`
+        );
+        res.sendFile(logoPath);
+        return;
       }
-      const providerLogosKeys = Object.keys(providerLogosJson)
+      const providerLogosKeys = Object.keys(providerLogosJson);
       if (!providerLogosKeys.length) {
-        console.log(`No logo heights specified in MOD_JSON file for '${provider}' provider`)
-        res.sendFile(logoPath)
-        return
+        console.log(
+          `No logo heights specified in MOD_JSON file for '${provider}' provider`
+        );
+        res.sendFile(logoPath);
+        return;
       }
-      const optimalHeightKey = getOptimalLogoHeightKey(providerLogosKeys, size, style)
-      const modLogoPath = providerLogosJson[optimalHeightKey].path
+      const optimalHeightKey = getOptimalLogoHeightKey(
+        providerLogosKeys,
+        size,
+        style
+      );
+      const modLogoPath = providerLogosJson[optimalHeightKey].path;
       if (!modLogoPath) {
-        console.log(`No logo path specified in MOD_JSON file for '${provider}' provider`)
-        res.sendFile(logoPath)
-        return
+        console.log(
+          `No logo path specified in MOD_JSON file for '${provider}' provider`
+        );
+        res.sendFile(logoPath);
+        return;
       }
-      res.sendFile(modLogoPath)
+      res.sendFile(modLogoPath);
     } catch (err) {
-      console.error(err)
-      res.status(400).json({ error: { message: 'Invalid Mod JSON file' } })
+      console.error(err);
+      res.status(400).json({ error: { message: "Invalid Mod JSON file" } });
     }
-  })
-}
+  });
+};

+ 13 - 13
server/api/router.ts

@@ -12,23 +12,23 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import express from 'express'
-import bodyParser from 'body-parser'
+import express from "express";
+import bodyParser from "body-parser";
 
-import LogosApi from './LogosApi'
-import ConfigApi from './ConfigApi'
+import LogosApi from "./LogosApi";
+import ConfigApi from "./ConfigApi";
 
-import packageJson from '../../package.json'
+import packageJson from "../../package.json";
 
-const router = express.Router()
+const router = express.Router();
 
-router.use(bodyParser.json())
+router.use(bodyParser.json());
 
-router.get('/version', (_, res) => {
-  res.json({ version: packageJson.version })
-})
+router.get("/version", (_, res) => {
+  res.json({ version: packageJson.version });
+});
 
-ConfigApi(router)
-LogosApi(router)
+ConfigApi(router);
+LogosApi(router);
 
-export default router
+export default router;

+ 4 - 3
server/index.js

@@ -1,3 +1,4 @@
-require('@babel/register')({ extensions: ['.ts', '.js'] })
-require('dotenv').config()
-require('./start')
+/* eslint-disable @typescript-eslint/no-var-requires */
+require("@babel/register")({ extensions: [".ts", ".js"] });
+require("dotenv").config();
+require("./start");

+ 15 - 15
server/main.ts

@@ -12,28 +12,28 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import express from 'express'
-import path from 'path'
+import express from "express";
+import path from "path";
 
-import apiRouter from './api/router'
-import proxyRouter from './proxies/router'
+import apiRouter from "./api/router";
+import proxyRouter from "./proxies/router";
 
 export default () => {
-  const app = express()
+  const app = express();
 
-  const PORT = process.env.PORT || 3000
+  const PORT = process.env.PORT || 3000;
 
-  app.use(express.static('dist'))
+  app.use(express.static("dist"));
 
-  app.use('/proxy', proxyRouter)
+  app.use("/proxy", proxyRouter);
 
-  app.use('/api', apiRouter)
+  app.use("/api", apiRouter);
 
-  app.get('*', (_, res) => {
-    res.sendFile(path.resolve(__dirname, '../dist', 'index.html'))
-  })
+  app.get("*", (_, res) => {
+    res.sendFile(path.resolve(__dirname, "../dist", "index.html"));
+  });
 
   app.listen(PORT, () => {
-    console.log(`Express server is up on port ${PORT}`)
-  })
-}
+    console.log(`Express server is up on port ${PORT}`);
+  });
+};

+ 59 - 39
server/proxies/azureProxy.ts

@@ -12,60 +12,80 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import express from 'express'
+import express from "express";
 
-import MsRest from 'ms-rest-azure'
-import axios from 'axios'
+import MsRest from "ms-rest-azure";
+import axios from "axios";
 
-const forwardHeaders = ['authorization']
+const forwardHeaders = ["authorization"];
 
 const buildError = (message: any) => ({
   error: { message: `Proxy - ${message}` },
-})
+});
 
 export default (router: express.Router) => {
-  router.post('/azure/login', (req, res) => {
+  router.post("/azure/login", (req, res) => {
     const handleResponse = (err: any, credentials: any) => {
       if (err) {
-        console.log(err)
-        res.status(401).send(buildError('Azure API authentication error'))
+        console.log(err);
+        res.status(401).send(buildError("Azure API authentication error"));
       } else {
-        res.send(credentials)
+        res.send(credentials);
       }
-    }
-    const connInfo = req.body
-    const userCred = connInfo.user_credentials
-    const servicePrin = connInfo.service_principal_credentials
+    };
+    const connInfo = req.body;
+    const userCred = connInfo.user_credentials;
+    const servicePrin = connInfo.service_principal_credentials;
     if (userCred && userCred.username && userCred.password) {
-      MsRest.loginWithUsernamePassword(userCred.username, userCred.password, handleResponse)
-    } else if (servicePrin && servicePrin.client_id && servicePrin.client_secret) {
-      MsRest.loginWithServicePrincipalSecret(servicePrin.client_id, servicePrin.client_secret, connInfo.tenant, handleResponse)
+      MsRest.loginWithUsernamePassword(
+        userCred.username,
+        userCred.password,
+        handleResponse
+      );
+    } else if (
+      servicePrin &&
+      servicePrin.client_id &&
+      servicePrin.client_secret
+    ) {
+      MsRest.loginWithServicePrincipalSecret(
+        servicePrin.client_id,
+        servicePrin.client_secret,
+        connInfo.tenant,
+        handleResponse
+      );
     } else {
-      res.status(401).send(buildError('Azure API authentication error'))
+      res.status(401).send(buildError("Azure API authentication error"));
     }
-  })
+  });
 
-  router.get('/azure/*', (req, res) => {
-    process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
-    const url = Buffer.from(req.url.substr('/proxy/'.length), 'base64').toString()
-    const headers: any = {}
-    forwardHeaders.forEach(headerName => {
+  router.get("/azure/*", (req, res) => {
+    process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
+    const url = Buffer.from(
+      req.url.substr("/proxy/".length),
+      "base64"
+    ).toString();
+    const headers: any = {};
+    forwardHeaders.forEach((headerName) => {
       if (req.headers[headerName] != null) {
-        headers[headerName] = req.headers[headerName]
+        headers[headerName] = req.headers[headerName];
       }
-    })
+    });
 
-    axios({ url, headers }).then(response => {
-      res.send(response.data)
-    }).catch(error => {
-      if (error.response) {
-        res.status(error.response.status).send(buildError(error.response.data.error.message))
-      } else if (error.request) {
-        console.log(error)
-        res.status(500).send(buildError('No Response!'))
-      } else {
-        res.status(500).send(buildError('Error creating request!'))
-      }
-    })
-  })
-}
+    axios({ url, headers })
+      .then((response) => {
+        res.send(response.data);
+      })
+      .catch((error) => {
+        if (error.response) {
+          res
+            .status(error.response.status)
+            .send(buildError(error.response.data.error.message));
+        } else if (error.request) {
+          console.log(error);
+          res.status(500).send(buildError("No Response!"));
+        } else {
+          res.status(500).send(buildError("Error creating request!"));
+        }
+      });
+  });
+};

+ 13 - 11
server/proxies/metalHubProxy.ts

@@ -12,24 +12,26 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import express from 'express'
-import fs from 'fs'
+import express from "express";
+import fs from "fs";
 
 const buildError = (message: any) => ({
   error: { message },
-})
+});
 
 export default (router: express.Router) => {
-  router.get('/metal-hub/fingerprint', async (_, res) => {
-    const path = process.env.CA_FINGERPRINT
+  router.get("/metal-hub/fingerprint", async (_, res) => {
+    const path = process.env.CA_FINGERPRINT;
     if (!path || !fs.existsSync(path)) {
-      res.status(500).json(buildError('Fingerprint path not configured properly'))
-      return
+      res
+        .status(500)
+        .json(buildError("Fingerprint path not configured properly"));
+      return;
     }
     try {
-      res.json(fs.readFileSync(path, 'utf8'))
+      res.json(fs.readFileSync(path, "utf8"));
     } catch (err) {
-      res.status(500).json(buildError(err.message))
+      res.status(500).json(buildError(err.message));
     }
-  })
-}
+  });
+};

+ 9 - 9
server/proxies/router.ts

@@ -12,16 +12,16 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import express from 'express'
-import bodyParser from 'body-parser'
-import metalHubProxy from './metalHubProxy'
-import azureProxy from './azureProxy'
+import express from "express";
+import bodyParser from "body-parser";
+import metalHubProxy from "./metalHubProxy";
+import azureProxy from "./azureProxy";
 
-const router = express.Router()
+const router = express.Router();
 
-router.use(bodyParser.json())
+router.use(bodyParser.json());
 
-azureProxy(router)
-metalHubProxy(router)
+azureProxy(router);
+metalHubProxy(router);
 
-export default router
+export default router;

+ 47 - 47
src/@types/Assessment.ts

@@ -12,67 +12,67 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import type { Endpoint } from './Endpoint'
-import type { Instance } from './Instance'
-import type { NetworkMap } from './Network'
+import type { Endpoint } from "./Endpoint";
+import type { Instance } from "./Instance";
+import type { NetworkMap } from "./Network";
 
 export type VmSize = {
-  name: string,
-  size?: string,
-}
+  name: string;
+  size?: string;
+};
 
 export type AzureLocation = {
-  id: string,
-  name: string,
-}
+  id: string;
+  name: string;
+};
 
 export type Group = {
-  id: string,
-  name: string,
-}
+  id: string;
+  name: string;
+};
 
 export type VmItem = {
-  id: string,
+  id: string;
   properties: {
-    recommendedSize: string,
+    recommendedSize: string;
     disks: {
       [diskName: string]: {
-        recommendedDiskType: string,
-      },
-    },
-    datacenterManagementServerName: string,
-    datacenterMachineArmId: string,
-    displayName: string,
-    operatingSystemName: string,
-  },
-}
+        recommendedDiskType: string;
+      };
+    };
+    datacenterManagementServerName: string;
+    datacenterMachineArmId: string;
+    displayName: string;
+    operatingSystemName: string;
+  };
+};
 
 export type Assessment = {
-  name: string,
-  id: string,
-  projectName: string,
-  resourceGroupName: string,
-  groupName: string,
-  assessmentName: string,
-  location: string,
+  name: string;
+  id: string;
+  projectName: string;
+  resourceGroupName: string;
+  groupName: string;
+  assessmentName: string;
+  location: string;
   project: {
-    name: string,
-  },
-  group: Group,
+    name: string;
+  };
+  group: Group;
   properties: {
-    status: string,
-    updatedTimestamp: string,
-    azureLocation: string,
-    numberOfMachines: string,
-  },
-  connectionInfo: { subscription_id: string } & Endpoint['connection_info'],
-}
+    status: string;
+    updatedTimestamp: string;
+    azureLocation: string;
+    numberOfMachines: string;
+  };
+  connectionInfo: { subscription_id: string } & Endpoint["connection_info"];
+};
 
 export type MigrationInfo = {
-  source: Endpoint | null,
-  target: Endpoint,
-  selectedInstances: Instance[],
-  fieldValues: { [fieldValue: string]: any },
-  networks: NetworkMap[],
-  vmSizes: { [vmSize: string]: VmSize },
-}
+  source: Endpoint | null;
+  target: Endpoint;
+  selectedInstances: Instance[];
+  fieldValues: { [fieldValue: string]: any };
+  networks: NetworkMap[];
+  vmSizes: { [vmSize: string]: VmSize };
+};

+ 4 - 4
src/@types/Cache.ts

@@ -1,6 +1,6 @@
 export type Cache = {
   [key: string]: {
-    data: any,
-    createdAt: string,
-  }
-}
+    data: any;
+    createdAt: string;
+  };
+};

+ 38 - 38
src/@types/Config.ts

@@ -1,46 +1,46 @@
-import { ProviderTypes } from './Providers'
+import { ProviderTypes } from "./Providers";
 
-type Type = 'source' | 'destination'
+type Type = "source" | "destination";
 
 type ExtraOption = {
-  name: string,
-  types: Type[],
-  requiredFields: string[],
-  relistFields?: string[],
+  name: string;
+  types: Type[];
+  requiredFields: string[];
+  relistFields?: string[];
   requiredValues?: {
-    field: string,
-    values: string[],
-  }[]
-}
+    field: string;
+    values: string[];
+  }[];
+};
 
 export type Services = {
-  keystone: string,
-  barbican: string,
-  coriolis: string,
-  coriolisLogs: string,
-  coriolisLogStreamBaseUrl: string,
-  coriolisLicensing: string,
-  metalhub: string,
-  cloudbaseEmailEndpoint: string
-}
+  keystone: string;
+  barbican: string;
+  coriolis: string;
+  coriolisLogs: string;
+  coriolisLogStreamBaseUrl: string;
+  coriolisLicensing: string;
+  metalhub: string;
+  cloudbaseEmailEndpoint: string;
+};
 
 export type Config = {
-  disabledPages: string[],
-  showUserDomainInput: boolean,
-  defaultUserDomain: string,
-  adminRoleName: string,
-  showOpenstackCurrentUserSwitch: boolean,
-  useBarbicanSecrets: boolean,
-  requestPollTimeout: number,
-  instancesListBackgroundLoading: { default: number, [prop: string]: number },
-  extraOptionsApiCalls: ExtraOption[],
-  providerSortPriority: { [providerName in ProviderTypes]: number },
-  providerNames: { [providerName in ProviderTypes]: string }
-  providersDisabledExecuteOptions: [ProviderTypes],
-  hiddenUsers: string[],
-  passwordFields: string[],
-  mainListItemsPerPage: number,
-  servicesUrls: Services,
-  maxMinionPoolEventsPerPage: number,
-  bareMetalEndpointName: string,
-}
+  disabledPages: string[];
+  showUserDomainInput: boolean;
+  defaultUserDomain: string;
+  adminRoleName: string;
+  showOpenstackCurrentUserSwitch: boolean;
+  useBarbicanSecrets: boolean;
+  requestPollTimeout: number;
+  instancesListBackgroundLoading: { default: number; [prop: string]: number };
+  extraOptionsApiCalls: ExtraOption[];
+  providerSortPriority: { [providerName in ProviderTypes]: number };
+  providerNames: { [providerName in ProviderTypes]: string };
+  providersDisabledExecuteOptions: [ProviderTypes];
+  hiddenUsers: string[];
+  passwordFields: string[];
+  mainListItemsPerPage: number;
+  servicesUrls: Services;
+  maxMinionPoolEventsPerPage: number;
+  bareMetalEndpointName: string;
+};

+ 57 - 48
src/@types/Endpoint.ts

@@ -12,79 +12,88 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import type { Disk } from './Instance'
-import type { ProviderTypes } from './Providers'
+import type { Disk } from "./Instance";
+import type { ProviderTypes } from "./Providers";
 
 export type Validation = {
-  valid: boolean,
-  message: string,
-}
+  valid: boolean;
+  message: string;
+};
 
 export type Endpoint = {
-  id: string,
-  name: string,
-  description: string,
-  type: ProviderTypes,
-  created_at: Date,
-  mapped_regions: string[],
+  id: string;
+  name: string;
+  description: string;
+  type: ProviderTypes;
+  created_at: Date;
+  mapped_regions: string[];
   connection_info: {
-    secret_ref?: string,
-    host?: string,
-    [prop: string]: any
-  },
-  [prop: string]: any
-}
+    secret_ref?: string;
+    host?: string;
+    [prop: string]: any;
+  };
+  [prop: string]: any;
+};
 
 export type MultiValidationItem = {
-  endpoint: Endpoint,
-  validation?: Validation,
-  validating: boolean,
-}
+  endpoint: Endpoint;
+  validation?: Validation;
+  validating: boolean;
+};
 
 export type OptionValues = {
-  name: string,
-  values: string[] | { name: string, id: string, [prop: string]: any }[],
-  config_default: string | { name: string, id: string },
-}
+  name: string;
+  values: string[] | { name: string; id: string; [prop: string]: any }[];
+  config_default: string | { name: string; id: string };
+};
 
 export type StorageBackend = {
-  id: string | null,
-  name: string,
+  id: string | null;
+  name: string;
   additional_provider_properties?: {
-    supported_bus_types?: string[]
-  }
-}
+    supported_bus_types?: string[];
+  };
+};
 
 export type Storage = {
-  storage_backends: StorageBackend[],
-  config_default?: string,
-}
+  storage_backends: StorageBackend[];
+  config_default?: string;
+};
 
 export type StorageMap = {
-  type: 'backend' | 'disk',
-  source: Disk,
-  target: StorageBackend,
-  targetBusType?: string | null
-}
+  type: "backend" | "disk";
+  source: Disk;
+  target: StorageBackend;
+  targetBusType?: string | null;
+};
 
 export const EndpointUtils = {
-  getBusTypeStorageId: (storageBackends: StorageBackend[], id: string | null): { busType: string | null, id: string | null } => {
-    const idMatches = /(.*):(.*)/.exec(String(id))
+  getBusTypeStorageId: (
+    storageBackends: StorageBackend[],
+    id: string | null
+  ): { busType: string | null; id: string | null } => {
+    const idMatches = /(.*):(.*)/.exec(String(id));
     if (!idMatches) {
-      return { busType: null, id }
+      return { busType: null, id };
     }
-    const actualId = idMatches[1]
-    const busType = idMatches[2]
+    const actualId = idMatches[1];
+    const busType = idMatches[2];
 
     for (let i = 0; i < storageBackends.length; i += 1) {
       if (storageBackends[i].id === actualId) {
-        if (storageBackends[i].additional_provider_properties?.supported_bus_types?.find(p => p === busType)) {
-          return { id: actualId, busType }
+        if (
+          storageBackends[
+            i
+          ].additional_provider_properties?.supported_bus_types?.find(
+            p => p === busType
+          )
+        ) {
+          return { id: actualId, busType };
         }
-        return { id: actualId, busType: null }
+        return { id: actualId, busType: null };
       }
     }
 
-    return { id, busType: null }
+    return { id, busType: null };
   },
-}
+};

+ 15 - 11
src/@types/Execution.ts

@@ -12,18 +12,22 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import { Task } from './Task'
+import { Task } from "./Task";
 
 export type Execution = {
-  id: string,
-  number: number,
-  status: string,
-  created_at: Date,
-  updated_at: Date,
-  deleted_at?: Date,
-  type: 'replica_execution' | 'replica_disks_delete' | 'replica_deploy' | 'replica_update'
-}
+  id: string;
+  number: number;
+  status: string;
+  created_at: Date;
+  updated_at: Date;
+  deleted_at?: Date;
+  type:
+    | "replica_execution"
+    | "replica_disks_delete"
+    | "replica_deploy"
+    | "replica_update";
+};
 
 export type ExecutionTasks = Execution & {
-  tasks: Task[]
-}
+  tasks: Task[];
+};

+ 80 - 75
src/@types/Field.ts

@@ -12,125 +12,130 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import { OptionsSchemaPlugin } from '@src/plugins'
-import LabelDictionary from '@src/utils/LabelDictionary'
-import { ProviderTypes } from './Providers'
+import { OptionsSchemaPlugin } from "@src/plugins";
+import LabelDictionary from "@src/utils/LabelDictionary";
+import { ProviderTypes } from "./Providers";
 
-type Separator = { separator: boolean }
+type Separator = { separator: boolean };
 type EnumItemObject = {
-  label?: string,
-  value?: any,
-  name?: string,
-  id?: string | null,
-  disabled?: boolean,
-  subtitleLabel?: string,
-}
-export const isEnumSeparator = (e: any): e is Separator => (typeof e !== 'string' && e.separator === true)
+  label?: string;
+  value?: unknown;
+  name?: string;
+  id?: string | null;
+  disabled?: boolean;
+  subtitleLabel?: string;
+  [key: string]: unknown;
+};
+export const isEnumSeparator = (e: any): e is Separator =>
+  typeof e !== "string" && e.separator === true;
 
-export type EnumItem = (
-  string | EnumItemObject | Separator
-)
+export type EnumItem = string | EnumItemObject | Separator;
 export type Field = {
-  name: string,
-  type?: string,
-  value?: any,
-  label?: string,
-  enum?: EnumItem[],
-  default?: any,
-  password?: boolean,
-  disabled?: boolean,
-  nullableBoolean?: boolean,
-  items?: Field[],
-  fields?: Field[],
-  minimum?: number,
-  maximum?: number,
-  parent?: string,
-  properties?: Field[],
-  required?: boolean,
-  useTextArea?: boolean,
-  useFile?: boolean,
-  readOnly?: boolean,
-  title?: string,
-  description?: string,
-  warning?: string,
-  subFields?: Field[],
-  groupName?: string
-}
+  name: string;
+  type?: string;
+  value?: any;
+  label?: string;
+  enum?: EnumItem[];
+  default?: any;
+  password?: boolean;
+  disabled?: boolean;
+  nullableBoolean?: boolean;
+  items?: Field[];
+  fields?: Field[];
+  minimum?: number;
+  maximum?: number;
+  parent?: string;
+  properties?: Field[];
+  required?: boolean;
+  useTextArea?: boolean;
+  useFile?: boolean;
+  readOnly?: boolean;
+  title?: string;
+  description?: string;
+  warning?: string;
+  subFields?: Field[];
+  groupName?: string;
+};
 
-const migrationImageOsTypes = ['windows', 'linux']
+const migrationImageOsTypes = ["windows", "linux"];
 
 class FieldHelper {
   getValueAlias(opts: {
-    name: string,
-    value: any,
-    fields: Field[],
-    targetProvider: ProviderTypes | null | undefined,
+    name: string;
+    value: any;
+    fields: Field[];
+    targetProvider: ProviderTypes | null | undefined;
   }): string {
-    const {
-      name, value, fields, targetProvider,
-    } = opts
-    const plugin = targetProvider && OptionsSchemaPlugin.for(targetProvider)
+    const { name, value, fields, targetProvider } = opts;
+    const plugin = targetProvider && OptionsSchemaPlugin.for(targetProvider);
 
     if (value === true) {
-      return 'Yes'
+      return "Yes";
     }
     if (value === false) {
-      return 'No'
+      return "No";
     }
-    const findField = (f: Field[]) => f.find(f1 => f1.name === name)
-    let field = findField(fields)
+    const findField = (f: Field[]) => f.find(f1 => f1.name === name);
+    let field = findField(fields);
     if (!field) {
       fields.forEach(f => {
         if (f.properties && !field) {
-          field = findField(f.properties)
+          field = findField(f.properties);
         }
 
         if (f.subFields && !field) {
-          field = findField(f.subFields)
+          field = findField(f.subFields);
           if (f.subFields && !field) {
             f.subFields.forEach(sf => {
               if (!field && sf.properties) {
-                field = findField(sf.properties)
+                field = findField(sf.properties);
               }
-            })
+            });
           }
         }
-      })
+      });
     }
     const findInEnum = (v: any) => {
-      let valueName = v
+      let valueName = v;
       if (field && field.enum) {
-        const enumObject: any = field.enum.find((e: any) => (e.id ? e.id === v : false))
+        const enumObject: any = field.enum.find((e: any) =>
+          e.id ? e.id === v : false
+        );
         if (enumObject && enumObject.name) {
-          valueName = enumObject.name
-        } else if (field && LabelDictionary.enumFields.find(f => field && f === field.name)) {
-          valueName = LabelDictionary.get(v)
+          valueName = enumObject.name;
+        } else if (
+          field &&
+          LabelDictionary.enumFields.find(f => field && f === field.name)
+        ) {
+          valueName = LabelDictionary.get(v);
         }
       }
-      return valueName
-    }
+      return valueName;
+    };
     if (value.join) {
-      return value.map((v: any) => findInEnum(v)).join(', ')
+      return value.map((v: any) => findInEnum(v)).join(", ");
     }
 
-    const isImageMapField = migrationImageOsTypes.find(os => os === name)
+    const isImageMapField = migrationImageOsTypes.find(os => os === name);
     if (isImageMapField) {
-      const migrImageField = plugin && fields
-        .find(f => f.name === plugin.migrationImageMapFieldName)
+      const migrImageField =
+        plugin &&
+        fields.find(f => f.name === plugin.migrationImageMapFieldName);
       if (migrImageField && migrImageField.properties) {
-        const imageField = migrImageField.properties.find(p => p.name === name)
+        const imageField = migrImageField.properties.find(p => p.name === name);
         if (imageField && imageField.enum) {
-          const imageFieldValueObject: any = imageField.enum
-            .find((e: any) => (e.id ? e.id === value : false))
+          const imageFieldValueObject: any = imageField.enum.find((e: any) =>
+            e.id ? e.id === value : false
+          );
           if (imageFieldValueObject) {
-            return imageFieldValueObject.name
+            return imageFieldValueObject.name;
           }
         }
       }
     }
 
-    return findInEnum(value)
+    return findInEnum(value);
   }
 }
 
-export default new FieldHelper()
+export default new FieldHelper();

+ 15 - 14
src/@types/InitialSetup.ts

@@ -1,22 +1,23 @@
-import { ProviderTypes } from './Providers'
+import { ProviderTypes } from "./Providers";
 
-export type SetupPageLicenceType = 'paid' | 'trial'
+export type SetupPageLicenceType = "paid" | "trial";
 
 export type CustomerInfoBasic = {
-  fullName: string,
-  email: string,
-  company: string,
-  country: string
-}
+  fullName: string;
+  email: string;
+  company: string;
+  country: string;
+};
 
 export type CustomerInfoTrial = {
-  interestedIn: 'replicas' | 'migrations' | 'both'
-  sourcePlatform: ProviderTypes | null
-  destinationPlatform: ProviderTypes | null
-}
+  interestedIn: "replicas" | "migrations" | "both";
+  sourcePlatform: ProviderTypes | null;
+  destinationPlatform: ProviderTypes | null;
+};
 
-export type CustomerInfoFull = CustomerInfoBasic & CustomerInfoTrial
+export type CustomerInfoFull = CustomerInfoBasic & CustomerInfoTrial;
 
 export const isCustomerInfoFull = (
-  customerInfo: CustomerInfoFull | CustomerInfoBasic,
-): customerInfo is CustomerInfoFull => (<CustomerInfoFull>customerInfo).interestedIn !== undefined
+  customerInfo: CustomerInfoFull | CustomerInfoBasic
+): customerInfo is CustomerInfoFull =>
+  (<CustomerInfoFull>customerInfo).interestedIn !== undefined;

+ 36 - 36
src/@types/Instance.ts

@@ -13,51 +13,51 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 export type Nic = {
-  id: string,
-  network_name: string,
-  ip_addresses?: string[],
-  mac_address: string,
-  network_id: string,
-}
+  id: string;
+  network_name: string;
+  ip_addresses?: string[];
+  mac_address: string;
+  network_id: string;
+};
 
 export type Disk = {
-  id: string,
-  name?: string,
-  storage_backend_identifier?: string,
-  format?: string,
-  guest_device?: string,
-  size_bytes?: number,
+  id: string;
+  name?: string;
+  storage_backend_identifier?: string;
+  format?: string;
+  guest_device?: string;
+  size_bytes?: number;
   disabled?: {
-    message: string,
-    info?: string,
-  },
-}
+    message: string;
+    info?: string;
+  };
+};
 
 export type Instance = {
-  id: string,
-  name: string,
-  flavor_name: string,
-  instance_name?: string | null,
-  num_cpu: number,
-  memory_mb: number,
-  os_type: string,
+  id: string;
+  name: string;
+  flavor_name: string;
+  instance_name?: string | null;
+  num_cpu: number;
+  memory_mb: number;
+  os_type: string;
   devices: {
-    nics: Nic[],
-    disks: Disk[],
-  },
-}
+    nics: Nic[];
+    disks: Disk[];
+  };
+};
 
 export type InstanceBase = {
-  id: string
-} & Partial<Instance>
+  id: string;
+} & Partial<Instance>;
 
 export type InstanceScript = {
-  global?: 'windows' | 'linux' | null,
-  instanceId?: string | null,
-  scriptContent: string | null,
-  fileName: string | null,
-}
+  global?: "windows" | "linux" | null;
+  instanceId?: string | null;
+  scriptContent: string | null;
+  fileName: string | null;
+};
 
 export const InstanceUtils = {
-  shortenId: (id: string) => id.replace(/(^.*?)-.*-(.*$)/, '$1-...-$2'),
-}
+  shortenId: (id: string) => id.replace(/(^.*?)-.*-(.*$)/, "$1-...-$2"),
+};

+ 17 - 17
src/@types/Licence.ts

@@ -13,22 +13,22 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 export type Licence = {
-  applianceId: string,
-  earliestLicenceExpiryDate: Date,
-  latestLicenceExpiryDate: Date,
-  currentPerformedMigrations: number,
-  currentPerformedReplicas: number,
-  lifetimePerformedMigrations: number,
-  lifetimePerformedReplicas: number,
-  currentAvailableMigrations: number,
-  currentAvailableReplicas: number,
-  lifetimeAvailableMigrations: number,
-  lifetimeAvailableReplicas: number,
-}
+  applianceId: string;
+  earliestLicenceExpiryDate: Date;
+  latestLicenceExpiryDate: Date;
+  currentPerformedMigrations: number;
+  currentPerformedReplicas: number;
+  lifetimePerformedMigrations: number;
+  lifetimePerformedReplicas: number;
+  currentAvailableMigrations: number;
+  currentAvailableReplicas: number;
+  lifetimeAvailableMigrations: number;
+  lifetimeAvailableReplicas: number;
+};
 
 export type LicenceServerStatus = {
-  hostname: string,
-  multi_appliance: boolean,
-  supported_licence_versions: string[],
-  server_local_time: string
-}
+  hostname: string;
+  multi_appliance: boolean;
+  supported_licence_versions: string[];
+  server_local_time: string;
+};

+ 2 - 2
src/@types/Log.ts

@@ -13,5 +13,5 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 export type Log = {
-  log_name: string,
-}
+  log_name: string;
+};

+ 93 - 84
src/@types/MainItem.ts

@@ -12,124 +12,133 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import type { Execution } from './Execution'
-import type { Instance, InstanceScript } from './Instance'
-import type { NetworkMap } from './Network'
-import type { StorageMap } from './Endpoint'
-import { Task } from './Task'
+import type { Execution } from "./Execution";
+import type { Instance, InstanceScript } from "./Instance";
+import type { NetworkMap } from "./Network";
+import type { StorageMap } from "./Endpoint";
+import { Task } from "./Task";
 
 export type MainItemInfo = {
   export_info: {
     devices: {
       nics: {
-        network_name: string,
-      }[],
-    },
-  },
-}
+        network_name: string;
+      }[];
+    };
+  };
+};
 
 export type UpdateData = {
-  destination: any,
-  source: any,
-  network: NetworkMap[],
-  storage: StorageMap[],
-  uploadedScripts: InstanceScript[],
-  removedScripts: InstanceScript[],
-}
-type NetworkMapSecurityGroups = { id: string, security_groups?: string[] }
+  destination: any;
+  source: any;
+  network: NetworkMap[];
+  storage: StorageMap[];
+  uploadedScripts: InstanceScript[];
+  removedScripts: InstanceScript[];
+};
+type NetworkMapSecurityGroups = { id: string; security_groups?: string[] };
 type NetworkMapSourceDest = {
   [prop: string]: {
-    source_network: string,
-    destination_network: string,
-  }
-}
-export const isNetworkMapSecurityGroups = (n: any): n is NetworkMapSecurityGroups => (typeof n !== 'string' && n && n.security_groups)
-export const isNetworkMapSourceDest = (n: any): n is NetworkMapSourceDest => (typeof n !== 'string' && (!n || !n.security_groups))
-export type TransferNetworkMap = NetworkMapSourceDest | string | NetworkMapSecurityGroups
+    source_network: string;
+    destination_network: string;
+  };
+};
+export const isNetworkMapSecurityGroups = (
+  n: any
+): n is NetworkMapSecurityGroups =>
+  typeof n !== "string" && n && n.security_groups;
+export const isNetworkMapSourceDest = (n: any): n is NetworkMapSourceDest =>
+  typeof n !== "string" && (!n || !n.security_groups);
+export type TransferNetworkMap =
+  | NetworkMapSourceDest
+  | string
+  | NetworkMapSecurityGroups;
 export type StorageMapping = {
   backend_mappings: {
-    destination: string,
-    source: string,
-  }[],
-  default: string | null,
-  disk_mappings: {
-    destination: string,
-    disk_id: string,
-  }[] | null,
-}
+    destination: string;
+    source: string;
+  }[];
+  default: string | null;
+  disk_mappings:
+    | {
+        destination: string;
+        disk_id: string;
+      }[]
+    | null;
+};
 
 type BaseItem = {
-  id: string,
-  name: string,
-  description?: string
-  notes: string,
-  created_at: string,
-  updated_at: string,
-  origin_endpoint_id: string,
-  destination_endpoint_id: string,
-  origin_minion_pool_id: string | null,
-  destination_minion_pool_id: string | null,
-  instances: string[],
-  info: { [prop: string]: MainItemInfo },
-  destination_environment: { [prop: string]: any },
-  source_environment: { [prop: string]: any },
-  transfer_result: { [prop: string]: Instance } | null,
-  replication_count?: number,
-  storage_mappings?: StorageMapping | null,
-  network_map?: TransferNetworkMap,
-  last_execution_status: string
-  user_id: string
-  instance_osmorphing_minion_pool_mappings?: { [instanceName: string]: string }
-  user_scripts?: UserScriptData
-}
+  id: string;
+  name: string;
+  description?: string;
+  notes: string;
+  created_at: string;
+  updated_at: string;
+  origin_endpoint_id: string;
+  destination_endpoint_id: string;
+  origin_minion_pool_id: string | null;
+  destination_minion_pool_id: string | null;
+  instances: string[];
+  info: { [prop: string]: MainItemInfo };
+  destination_environment: { [prop: string]: any };
+  source_environment: { [prop: string]: any };
+  transfer_result: { [prop: string]: Instance } | null;
+  replication_count?: number;
+  storage_mappings?: StorageMapping | null;
+  network_map?: TransferNetworkMap;
+  last_execution_status: string;
+  user_id: string;
+  instance_osmorphing_minion_pool_mappings?: { [instanceName: string]: string };
+  user_scripts?: UserScriptData;
+};
 
 export type ReplicaItem = BaseItem & {
-  type: 'replica',
-}
+  type: "replica";
+};
 
 export type UserScriptData = {
   global?: {
-    linux?: string | null
-    windows?: string | null
-  }
+    linux?: string | null;
+    windows?: string | null;
+  };
   instances?: {
-    [instanceName: string]: string | null
-  }
-}
+    [instanceName: string]: string | null;
+  };
+};
 
 export type MigrationItem = BaseItem & {
-  type: 'migration',
-  replica_id?: string,
-}
+  type: "migration";
+  replica_id?: string;
+};
 
 export type MigrationItemOptions = MigrationItem & {
-  skip_os_morphing: boolean,
-  shutdown_instances: boolean,
-}
+  skip_os_morphing: boolean;
+  shutdown_instances: boolean;
+};
 
-export type TransferItem = ReplicaItem | MigrationItem
+export type TransferItem = ReplicaItem | MigrationItem;
 
 export type ReplicaItemDetails = ReplicaItem & {
-  executions: Execution[],
-}
+  executions: Execution[];
+};
 
 export type MigrationItemDetails = MigrationItem & {
-  tasks: Task[]
-}
+  tasks: Task[];
+};
 
-export type TransferItemDetails = ReplicaItemDetails | MigrationItemDetails
+export type TransferItemDetails = ReplicaItemDetails | MigrationItemDetails;
 
 export const getTransferItemTitle = (item: TransferItem | null) => {
   if (!item) {
-    return null
+    return null;
   }
-  const { instances, notes } = item
-  let title = notes
+  const { instances, notes } = item;
+  let title = notes;
   if (!notes) {
-    title = instances[0]
+    title = instances[0];
     if (instances.length > 1) {
-      title += ` (+${instances.length - 1} more)`
+      title += ` (+${instances.length - 1} more)`;
     }
   }
-  return title
-}
+  return title;
+};

+ 34 - 34
src/@types/MetalHub.ts

@@ -1,41 +1,41 @@
 export type MetalHubDisk = {
-  id: string,
-  path: string,
-  name: string,
-  size: number,
-  physical_sector_size: number,
+  id: string;
+  path: string;
+  name: string;
+  size: number;
+  physical_sector_size: number;
   partitions: {
-    name: string,
-    path: string,
-    partition_uuid: string,
-    sectors: number,
-    start_sector: number,
-    end_sector: number,
-  }[],
-}
+    name: string;
+    path: string;
+    partition_uuid: string;
+    sectors: number;
+    start_sector: number;
+    end_sector: number;
+  }[];
+};
 
 export type MetalHubNic = {
-  interface_type: string,
-  ip_addresses: string[],
-  mac_address: string,
-  nic_name: string,
-}
+  interface_type: string;
+  ip_addresses: string[];
+  mac_address: string;
+  nic_name: string;
+};
 
 export type MetalHubServer = {
-  id: number,
-  active: boolean,
-  hostname?: string,
-  created_at: string,
-  updated_at: string,
-  api_endpoint: string,
-  firmware_type?: string,
-  memory?: number,
+  id: number;
+  active: boolean;
+  hostname?: string;
+  created_at: string;
+  updated_at: string;
+  api_endpoint: string;
+  firmware_type?: string;
+  memory?: number;
   os_info: {
-    os_name: string,
-    os_version: string,
-  },
-  disks?: MetalHubDisk[],
-  nics?: MetalHubNic[],
-  physical_cores?: number,
-  logical_cores?: number,
-}
+    os_name: string;
+    os_version: string;
+  };
+  disks?: MetalHubDisk[];
+  nics?: MetalHubNic[];
+  physical_cores?: number;
+  logical_cores?: number;
+};

+ 43 - 41
src/@types/MinionPool.ts

@@ -13,49 +13,51 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 export type MinionMachine = {
-  id: string
-  created_at: string
-  updated_at: string
-  allocation_status: string
-  connection_info?: any
-  power_status: string
-  provider_properties: any
-  last_used_at?: string
-  allocated_action: string | null
-}
+  id: string;
+  created_at: string;
+  updated_at: string;
+  allocation_status: string;
+  connection_info?: any;
+  power_status: string;
+  provider_properties: any;
+  last_used_at?: string;
+  allocated_action: string | null;
+};
 export type MinionPoolEvent = {
-  id: string
-  index: number
-  level: 'INFO' | 'DEBUG' | 'ERROR'
-  message: string
-  created_at: string
-}
+  id: string;
+  index: number;
+  level: "INFO" | "DEBUG" | "ERROR";
+  message: string;
+  created_at: string;
+};
 export type MinionPoolProgressUpdate = {
-  id: string
-  current_step: number
-  message: string
-  created_at: string
-}
-export type MinionPoolEventProgressUpdate = MinionPoolEvent | MinionPoolProgressUpdate
+  id: string;
+  current_step: number;
+  message: string;
+  created_at: string;
+};
+export type MinionPoolEventProgressUpdate =
+  | MinionPoolEvent
+  | MinionPoolProgressUpdate;
 export type MinionPool = {
-  id: string
-  created_at: string
-  updated_at: string | null
-  name: string
-  os_type: 'linux' | 'windows'
-  status: string
-  minimum_minions: number
-  maximum_minions: number
-  environment_options: { [prop: string]: any }
-  endpoint_id: string
-  notes?: string
-  platform: 'source' | 'destination',
-  minion_machines: MinionMachine[],
-  minion_retention_strategy: 'poweroff' | 'delete'
-  minion_max_idle_time: number,
-}
+  id: string;
+  created_at: string;
+  updated_at: string | null;
+  name: string;
+  os_type: "linux" | "windows";
+  status: string;
+  minimum_minions: number;
+  maximum_minions: number;
+  environment_options: { [prop: string]: any };
+  endpoint_id: string;
+  notes?: string;
+  platform: "source" | "destination";
+  minion_machines: MinionMachine[];
+  minion_retention_strategy: "poweroff" | "delete";
+  minion_max_idle_time: number;
+};
 
 export type MinionPoolDetails = MinionPool & {
-  events: MinionPoolEvent[],
-  progress_updates: MinionPoolProgressUpdate[]
-}
+  events: MinionPoolEvent[];
+  progress_updates: MinionPoolProgressUpdate[];
+};

+ 29 - 24
src/@types/Network.ts

@@ -12,47 +12,52 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import type { Nic } from './Instance'
+import type { Nic } from "./Instance";
 
-export type SecurityGroup = string | {
-  id: string,
-  name: string,
-}
+export type SecurityGroup =
+  | string
+  | {
+      id: string;
+      name: string;
+    };
 
 export type Network = {
-  name: string,
-  id: string,
+  name: string;
+  id: string;
   // The `security_groups` field is currently used only by OCI
-  security_groups?: SecurityGroup[],
+  security_groups?: SecurityGroup[];
   // The `port_keys` field is currenlty used only by VMWare
-  port_keys?: string[],
-}
+  port_keys?: string[];
+};
 
 export type NetworkMap = {
-  sourceNic: Nic,
-  targetNetwork: Network | null,
-  targetSecurityGroups?: SecurityGroup[] | null,
-  targetPortKey?: string | null
-}
+  sourceNic: Nic;
+  targetNetwork: Network | null;
+  targetSecurityGroups?: SecurityGroup[] | null;
+  targetPortKey?: string | null;
+};
 
 export const NetworkUtils = {
-  getPortKeyNetworkId: (networks: Network[], id: string): { portKey: string | null, id: string } => {
-    const idMatches = /(.*):(.*)/.exec(String(id))
+  getPortKeyNetworkId: (
+    networks: Network[],
+    id: string
+  ): { portKey: string | null; id: string } => {
+    const idMatches = /(.*):(.*)/.exec(String(id));
     if (!idMatches) {
-      return { portKey: null, id }
+      return { portKey: null, id };
     }
-    const actualId = idMatches[1]
-    const portKey = idMatches[2]
+    const actualId = idMatches[1];
+    const portKey = idMatches[2];
 
     for (let i = 0; i < networks.length; i += 1) {
       if (networks[i].id === actualId) {
         if (networks[i].port_keys?.find(p => p === portKey)) {
-          return { id: actualId, portKey }
+          return { id: actualId, portKey };
         }
-        return { id: actualId, portKey: null }
+        return { id: actualId, portKey: null };
       }
     }
 
-    return { id, portKey: null }
+    return { id, portKey: null };
   },
-}
+};

+ 22 - 22
src/@types/NotificationItem.ts

@@ -12,34 +12,34 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-export type AlertInfoLevel = 'success' | 'error' | 'info' | 'warning'
+export type AlertInfoLevel = "success" | "error" | "info" | "warning";
 
 export type AlertInfoOptions = {
   action?: {
-    label: string,
-    callback: () => any,
-  }
-}
+    label: string;
+    callback: () => any;
+  };
+};
 
 export type AlertInfo = {
-  options?: AlertInfoOptions | null,
-  message: string,
-  title?: string,
-  id?: string,
-  level?: AlertInfoLevel,
-}
+  options?: AlertInfoOptions | null;
+  message: string;
+  title?: string;
+  id?: string;
+  level?: AlertInfoLevel;
+};
 
 export type NotificationItemData = {
-  id: string,
-  name: string,
-  description: string,
-  type: string,
-  status: string,
-  unseen?: boolean,
-  updatedAt?: string,
-}
+  id: string;
+  name: string;
+  description: string;
+  type: string;
+  status: string;
+  unseen?: boolean;
+  updatedAt?: string;
+};
 
 export type NotificationItem = {
-  projectId: string,
-  items: NotificationItemData[],
-}
+  projectId: string;
+  items: NotificationItemData[];
+};

+ 19 - 19
src/@types/Project.ts

@@ -13,30 +13,30 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 export type Project = {
-  id: string,
-  name: string,
-  enabled?: boolean,
-  description?: string,
-}
+  id: string;
+  name: string;
+  enabled?: boolean;
+  description?: string;
+};
 
 export type Role = {
-  id: string,
-  name: string,
-}
+  id: string;
+  name: string;
+};
 
 export type RoleAssignment = {
   scope: {
     project?: {
-      id: string,
-      name: string,
-    },
-  },
+      id: string;
+      name: string;
+    };
+  };
   role: {
-    id: string,
-    name: string,
-  },
+    id: string;
+    name: string;
+  };
   user: {
-    id: string,
-    name: string,
-  },
-}
+    id: string;
+    name: string;
+  };
+};

+ 17 - 4
src/@types/Providers.ts

@@ -12,10 +12,23 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-export type ProviderTypes = 'azure' | 'openstack' | 'opc' | 'oracle_vm' | 'vmware_vsphere' | 'aws' | 'oci' | 'hyper-v' | 'scvmm' | 'olvm' | 'kubevirt' | 'metal' | 'rhev'
+export type ProviderTypes =
+  | "azure"
+  | "openstack"
+  | "opc"
+  | "oracle_vm"
+  | "vmware_vsphere"
+  | "aws"
+  | "oci"
+  | "hyper-v"
+  | "scvmm"
+  | "olvm"
+  | "kubevirt"
+  | "metal"
+  | "rhev";
 
 export type Providers = {
   [provider in ProviderTypes]: {
-    types: number[],
-  }
-}
+    types: number[];
+  };
+};

+ 6 - 6
src/@types/Region.ts

@@ -13,9 +13,9 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 export type Region = {
-  id: string,
-  name: string,
-  description: string,
-  enabled: boolean,
-  mapped_endpoints: string[],
-}
+  id: string;
+  name: string;
+  description: string;
+  enabled: boolean;
+  mapped_endpoints: string[];
+};

+ 16 - 16
src/@types/Schedule.ts

@@ -12,25 +12,25 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-export type ScheduleFieldName = 'hour' | 'minute' | 'month' | 'dow' | 'dom'
+export type ScheduleFieldName = "hour" | "minute" | "month" | "dow" | "dom";
 
 export type ScheduleInfo = {
-  hour?: number,
-  minute?: number,
-  month?: number,
-  dow?: number,
-  dom?: number,
-}
+  hour?: number;
+  minute?: number;
+  month?: number;
+  dow?: number;
+  dom?: number;
+};
 
 export type Schedule = {
-  id?: string,
-  enabled?: boolean | null,
-  schedule?: ScheduleInfo,
-  expiration_date?: Date,
-  shutdown_instances?: boolean,
-}
+  id?: string;
+  enabled?: boolean | null;
+  schedule?: ScheduleInfo;
+  expiration_date?: Date;
+  shutdown_instances?: boolean;
+};
 
 export type ScheduleBulkItem = {
-  replicaId: string,
-  schedules: Schedule[],
-}
+  replicaId: string;
+  schedules: Schedule[];
+};

+ 24 - 21
src/@types/Schema.ts

@@ -24,28 +24,31 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 export type SchemaProperties = {
   properties: {
-    [prop: string]: {
-      type: 'array',
-      items: {
-        type: string,
-      },
-    } | {
-      type: string,
-      enum?: string[],
-      default?: any,
-    } | {
-      $ref: string,
-    },
-  },
-  required: string[],
-  type?: string,
-}
+    [prop: string]:
+      | {
+          type: "array";
+          items: {
+            type: string;
+          };
+        }
+      | {
+          type: string;
+          enum?: string[];
+          default?: any;
+        }
+      | {
+          $ref: string;
+        };
+  };
+  required: string[];
+  type?: string;
+};
 
 export type SchemaDefinitions = {
-  [prop: string]: SchemaProperties,
-}
+  [prop: string]: SchemaProperties;
+};
 
 export type Schema = {
-  oneOf: SchemaProperties[],
-  definitions?: SchemaDefinitions,
-}
+  oneOf: SchemaProperties[];
+  definitions?: SchemaDefinitions;
+};

+ 16 - 16
src/@types/Task.ts

@@ -13,21 +13,21 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 export type ProgressUpdate = {
-  index: number,
-  message: string,
-  created_at: Date,
-  total_steps: number | null
-  current_step: number | null
-}
+  index: number;
+  message: string;
+  created_at: Date;
+  total_steps: number | null;
+  current_step: number | null;
+};
 
 export type Task = {
-  id: string,
-  status: string,
-  created_at: Date,
-  updated_at: Date,
-  progress_updates: ProgressUpdate[],
-  task_type: string,
-  instance: string,
-  depends_on: string[],
-  exception_details: string,
-}
+  id: string;
+  status: string;
+  created_at: Date;
+  updated_at: Date;
+  progress_updates: ProgressUpdate[];
+  task_type: string;
+  instance: string;
+  depends_on: string[];
+  exception_details: string;
+};

+ 19 - 19
src/@types/User.ts

@@ -12,26 +12,26 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import type { Project } from './Project'
+import type { Project } from "./Project";
 
 export type User = {
-  scoped?: boolean,
-  project: Project,
-  email: string,
-  name: string,
-  id: string,
-  description?: string,
-  enabled?: boolean,
-  project_id?: string,
-  domain_id?: string,
-  isAdmin?: boolean | null,
-  password?: string,
-  extra?: any,
-  token?: string
-}
+  scoped?: boolean;
+  project: Project;
+  email: string;
+  name: string;
+  id: string;
+  description?: string;
+  enabled?: boolean;
+  project_id?: string;
+  domain_id?: string;
+  isAdmin?: boolean | null;
+  password?: string;
+  extra?: any;
+  token?: string;
+};
 
 export type Credentials = {
-  name: string,
-  password: string,
-  domain: string,
-}
+  name: string;
+  password: string;
+  domain: string;
+};

+ 15 - 15
src/@types/WizardData.ts

@@ -12,22 +12,22 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import type { Instance } from './Instance'
-import type { NetworkMap } from './Network'
-import type { Endpoint } from './Endpoint'
+import type { Instance } from "./Instance";
+import type { NetworkMap } from "./Network";
+import type { Endpoint } from "./Endpoint";
 
 export type WizardData = {
-  destOptions?: { [prop: string]: any } | null,
-  sourceOptions?: { [prop: string]: any } | null,
-  selectedInstances?: Instance[] | null,
-  networks?: NetworkMap[] | null,
-  source?: Endpoint | null,
-  target?: Endpoint | null,
-}
+  destOptions?: { [prop: string]: any } | null;
+  sourceOptions?: { [prop: string]: any } | null;
+  selectedInstances?: Instance[] | null;
+  networks?: NetworkMap[] | null;
+  source?: Endpoint | null;
+  target?: Endpoint | null;
+};
 
 export type WizardPage = {
-  id: string,
-  title: string,
-  breadcrumb: string,
-  excludeFrom?: 'replica' | 'migration',
-}
+  id: string;
+  title: string;
+  breadcrumb: string;
+  excludeFrom?: "replica" | "migration";
+};

+ 10 - 10
src/@types/declarations.d.ts

@@ -1,19 +1,19 @@
-declare module 'imgur'
+declare module "imgur";
 
-declare module '*.png'
-declare module '*.jpg'
-declare module '*.svg'
-declare module '*.woff'
+declare module "*.png";
+declare module "*.jpg";
+declare module "*.svg";
+declare module "*.woff";
 
-declare module 'ansi-to-html'
+declare module "ansi-to-html";
 
-declare module 'require-without-cache'
-declare module 'react-transition-group'
-declare module 'tai-password-strength'
+declare module "require-without-cache";
+declare module "react-transition-group";
+declare module "tai-password-strength";
 
 interface Window {
   /**
    * Needed for KeyboardManager conflict resolution
    */
-  handlingEnterKey: boolean | undefined
+  handlingEnterKey: boolean | undefined;
 }

+ 146 - 113
src/components/App.tsx

@@ -12,46 +12,44 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import { hot } from 'react-hot-loader/root'
-import React from 'react'
-import {
-  BrowserRouter as Router, Switch, Route,
-} from 'react-router-dom'
-import styled, { createGlobalStyle } from 'styled-components'
-import { observe } from 'mobx'
+import { hot } from "react-hot-loader/root";
+import React from "react";
+import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
+import styled, { createGlobalStyle } from "styled-components";
+import { observe } from "mobx";
 
-import Fonts from '@src/components/ui/Fonts'
-import NotificationsModule from '@src/components/modules/NotificationsModule'
-import LoginPage from '@src/components/smart/LoginPage'
-import ReplicasPage from '@src/components/smart/ReplicasPage'
-import MessagePage from '@src/components/smart/MessagePage'
-import ReplicaDetailsPage from '@src/components/smart/ReplicaDetailsPage'
-import MigrationsPage from '@src/components/smart/MigrationsPage'
-import MigrationDetailsPage from '@src/components/smart/MigrationDetailsPage'
-import MetalHubServersPage from '@src/components/smart/MetalHubServersPage'
-import EndpointsPage from '@src/components/smart/EndpointsPage'
-import EndpointDetailsPage from '@src/components/smart/EndpointDetailsPage'
-import AssessmentsPage from '@src/components/smart/AssessmentsPage'
-import AssessmentDetailsPage from '@src/components/smart/AssessmentDetailsPage'
-import UsersPage from '@src/components/smart/UsersPage'
-import UserDetailsPage from '@src/components/smart/UserDetailsPage'
-import ProjectsPage from '@src/components/smart/ProjectsPage'
-import ProjectDetailsPage from '@src/components/smart/ProjectDetailsPage'
-import DashboardPage from '@src/components/smart/DashboardPage'
-import LogsPage from '@src/components/smart/LogsPage'
-import LogStreamPage from '@src/components/smart/LogStreamPage'
-import WizardPage from '@src/components/smart/WizardPage'
+import Fonts from "@src/components/ui/Fonts";
+import NotificationsModule from "@src/components/modules/NotificationsModule";
+import LoginPage from "@src/components/smart/LoginPage";
+import ReplicasPage from "@src/components/smart/ReplicasPage";
+import MessagePage from "@src/components/smart/MessagePage";
+import ReplicaDetailsPage from "@src/components/smart/ReplicaDetailsPage";
+import MigrationsPage from "@src/components/smart/MigrationsPage";
+import MigrationDetailsPage from "@src/components/smart/MigrationDetailsPage";
+import MetalHubServersPage from "@src/components/smart/MetalHubServersPage";
+import EndpointsPage from "@src/components/smart/EndpointsPage";
+import EndpointDetailsPage from "@src/components/smart/EndpointDetailsPage";
+import AssessmentsPage from "@src/components/smart/AssessmentsPage";
+import AssessmentDetailsPage from "@src/components/smart/AssessmentDetailsPage";
+import UsersPage from "@src/components/smart/UsersPage";
+import UserDetailsPage from "@src/components/smart/UserDetailsPage";
+import ProjectsPage from "@src/components/smart/ProjectsPage";
+import ProjectDetailsPage from "@src/components/smart/ProjectDetailsPage";
+import DashboardPage from "@src/components/smart/DashboardPage";
+import LogsPage from "@src/components/smart/LogsPage";
+import LogStreamPage from "@src/components/smart/LogStreamPage";
+import WizardPage from "@src/components/smart/WizardPage";
 
-import Tooltip from '@src/components/ui/Tooltip'
+import Tooltip from "@src/components/ui/Tooltip";
 
-import MinionPoolsPage from '@src/components/smart/MinionPoolsPage'
-import MinionPoolDetailsPage from '@src/components/smart/MinionPoolDetailsPage'
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import configLoader from '@src/utils/Config'
-import { navigationMenu } from '@src/constants'
-import userStore from '@src/stores/UserStore'
-import SetupPage from '@src/components/smart/SetupPage'
-import MetalHubServerDetailsPage from '@src/components/smart/MetalHubServerDetailsPage'
+import MinionPoolsPage from "@src/components/smart/MinionPoolsPage";
+import MinionPoolDetailsPage from "@src/components/smart/MinionPoolDetailsPage";
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
+import configLoader from "@src/utils/Config";
+import { navigationMenu } from "@src/constants";
+import userStore from "@src/stores/UserStore";
+import SetupPage from "@src/components/smart/SetupPage";
+import MetalHubServerDetailsPage from "@src/components/smart/MetalHubServerDetailsPage";
 
 const GlobalStyle = createGlobalStyle`
  ${Fonts}
@@ -70,9 +68,9 @@ const GlobalStyle = createGlobalStyle`
     -webkit-font-smoothing: antialiased;
     -moz-osx-font-smoothing: grayscale;
   }
-`
+`;
 
-const Wrapper = styled.div<any>`
+const Wrapper = styled.div`
   height: 100%;
   min-height: 0;
   display: flex;
@@ -80,45 +78,45 @@ const Wrapper = styled.div<any>`
   > div:first-child {
     height: 100%;
   }
-`
+`;
 
 type State = {
-  isConfigReady: boolean,
-}
+  isConfigReady: boolean;
+};
 
-class App extends React.Component<{}, State> {
+class App extends React.Component<Record<string, unknown>, State> {
   state = {
     isConfigReady: false,
-  }
+  };
 
   async componentDidMount() {
-    observe(userStore, 'loggedUser', () => {
-      this.setState({})
-    })
-    await configLoader.load()
-    if (configLoader.isFirstLaunch && window.location.pathname !== '/login') {
-      if (window.location.pathname !== '/') {
-        window.location.href = '/'
-        return
+    observe(userStore, "loggedUser", () => {
+      this.setState({});
+    });
+    await configLoader.load();
+    if (configLoader.isFirstLaunch && window.location.pathname !== "/login") {
+      if (window.location.pathname !== "/") {
+        window.location.href = "/";
+        return;
       }
     } else {
-      userStore.tokenLogin()
+      userStore.tokenLogin();
     }
-    this.setState({ isConfigReady: true })
+    this.setState({ isConfigReady: true });
   }
 
   render() {
     if (!this.state.isConfigReady) {
-      return null
+      return null;
     }
 
     const renderMessagePage = (options: {
-      path: string,
-      exact?: boolean,
-      title: string,
-      subtitle: string,
-      showAuthAnimation?: boolean,
-      showDenied?: boolean,
+      path: string;
+      exact?: boolean;
+      title: string;
+      subtitle: string;
+      showAuthAnimation?: boolean;
+      showDenied?: boolean;
     }) => (
       <Route
         path={options.path}
@@ -133,58 +131,68 @@ class App extends React.Component<{}, State> {
           />
         )}
       />
-    )
+    );
 
-    const renderRoute = (path: string, component: any, exact?: boolean) => {
+    const renderRoute = (
+      path: string,
+      component: React.ReactNode,
+      exact?: boolean
+    ) => {
       if (!userStore.loggedUser) {
         return renderMessagePage({
           path,
           exact,
-          title: 'Authenticating...',
-          subtitle: 'Please wait while authenticating user.',
+          title: "Authenticating...",
+          subtitle: "Please wait while authenticating user.",
           showAuthAnimation: true,
-        })
+        });
       }
       // @ts-ignore
-      return <Route path={path} component={component} exact={exact} />
-    }
+      return <Route path={path} component={component} exact={exact} />;
+    };
 
-    const renderOptionalRoute = (opts: { name: string, component: any, path?: string, exact?: boolean }) => {
-      const {
-        name, component, path, exact,
-      } = opts
+    const renderOptionalRoute = (opts: {
+      name: string;
+      component: React.ReactNode;
+      path?: string;
+      exact?: boolean;
+    }) => {
+      const { name, component, path, exact } = opts;
       if (configLoader.config.disabledPages.find(p => p === name)) {
-        return null
+        return null;
       }
-      const actualPath = `${path || `/${name}`}`
-      const requiresAdmin = Boolean(navigationMenu.find(n => n.value === name && n.requiresAdmin))
+      const actualPath = `${path || `/${name}`}`;
+      const requiresAdmin = Boolean(
+        navigationMenu.find(n => n.value === name && n.requiresAdmin)
+      );
       if (!requiresAdmin) {
-        return renderRoute(actualPath, component, exact)
+        return renderRoute(actualPath, component, exact);
       }
       if (!userStore.loggedUser || userStore.loggedUser.isAdmin == null) {
         return renderMessagePage({
           path: actualPath,
           exact,
-          title: 'Checking permissions...',
-          subtitle: 'Please wait while checking user\'s permissions.',
+          title: "Checking permissions...",
+          subtitle: "Please wait while checking user's permissions.",
           showAuthAnimation: true,
-        })
+        });
       }
       if (userStore.loggedUser?.isAdmin === false) {
         return renderMessagePage({
           path: actualPath,
           exact,
-          title: 'User doesn\'t have permissions to view this page',
-          subtitle: 'Please login in with an administrator acount to view this page.',
+          title: "User doesn't have permissions to view this page",
+          subtitle:
+            "Please login in with an administrator acount to view this page.",
           showDenied: true,
-        })
+        });
       }
       if (userStore.loggedUser?.isAdmin) {
         // @ts-ignore
-        return <Route path={actualPath} exact={exact} component={component} />
+        return <Route path={actualPath} exact={exact} component={component} />;
       }
-      return null
-    }
+      return null;
+    };
 
     return (
       <Wrapper>
@@ -192,37 +200,62 @@ class App extends React.Component<{}, State> {
         <Router>
           <Switch>
             {configLoader.isFirstLaunch ? (
-            // @ts-ignore
+              // @ts-ignore
               <Route path="/" component={SetupPage} exact />
-            // @ts-ignore
-            ) : renderRoute('/', DashboardPage, true)}
+            ) : (
+              // @ts-ignore
+              renderRoute("/", DashboardPage, true)
+            )}
             {
               // @ts-ignore
               <Route path="/login" component={LoginPage} />
             }
-            {renderRoute('/dashboard', DashboardPage)}
-            {renderRoute('/replicas', ReplicasPage, true)}
-            {renderRoute('/replicas/:id', ReplicaDetailsPage, true)}
-            {renderRoute('/replicas/:id/:page', ReplicaDetailsPage)}
-            {renderRoute('/migrations', MigrationsPage, true)}
-            {renderRoute('/migrations/:id', MigrationDetailsPage, true)}
-            {renderRoute('/migrations/:id/:page', MigrationDetailsPage)}
-            {renderRoute('/endpoints', EndpointsPage, true)}
-            {renderRoute('/endpoints/:id', EndpointDetailsPage)}
-            {renderRoute('/minion-pools', MinionPoolsPage, true)}
-            {renderRoute('/minion-pools/:id', MinionPoolDetailsPage, true)}
-            {renderRoute('/minion-pools/:id/:page', MinionPoolDetailsPage)}
-            {renderRoute('/bare-metal-servers', MetalHubServersPage, true)}
-            {renderRoute('/bare-metal-servers/:id', MetalHubServerDetailsPage)}
-            {renderRoute('/wizard/:type', WizardPage)}
-            {renderOptionalRoute({ name: 'planning', component: AssessmentsPage })}
-            {renderOptionalRoute({ name: 'planning', component: AssessmentDetailsPage, path: '/assessment/:info' })}
-            {renderOptionalRoute({ name: 'users', component: UsersPage, exact: true })}
-            {renderOptionalRoute({ name: 'users', component: UserDetailsPage, path: '/users/:id' })}
-            {renderOptionalRoute({ name: 'projects', component: ProjectsPage, exact: true })}
-            {renderOptionalRoute({ name: 'projects', component: ProjectDetailsPage, path: '/projects/:id' })}
-            {renderOptionalRoute({ name: 'logging', component: LogsPage })}
-            {renderRoute('/streamlog', LogStreamPage)}
+            {renderRoute("/dashboard", DashboardPage)}
+            {renderRoute("/replicas", ReplicasPage, true)}
+            {renderRoute("/replicas/:id", ReplicaDetailsPage, true)}
+            {renderRoute("/replicas/:id/:page", ReplicaDetailsPage)}
+            {renderRoute("/migrations", MigrationsPage, true)}
+            {renderRoute("/migrations/:id", MigrationDetailsPage, true)}
+            {renderRoute("/migrations/:id/:page", MigrationDetailsPage)}
+            {renderRoute("/endpoints", EndpointsPage, true)}
+            {renderRoute("/endpoints/:id", EndpointDetailsPage)}
+            {renderRoute("/minion-pools", MinionPoolsPage, true)}
+            {renderRoute("/minion-pools/:id", MinionPoolDetailsPage, true)}
+            {renderRoute("/minion-pools/:id/:page", MinionPoolDetailsPage)}
+            {renderRoute("/bare-metal-servers", MetalHubServersPage, true)}
+            {renderRoute("/bare-metal-servers/:id", MetalHubServerDetailsPage)}
+            {renderRoute("/wizard/:type", WizardPage)}
+            {renderOptionalRoute({
+              name: "planning",
+              component: AssessmentsPage,
+            })}
+            {renderOptionalRoute({
+              name: "planning",
+              component: AssessmentDetailsPage,
+              path: "/assessment/:info",
+            })}
+            {renderOptionalRoute({
+              name: "users",
+              component: UsersPage,
+              exact: true,
+            })}
+            {renderOptionalRoute({
+              name: "users",
+              component: UserDetailsPage,
+              path: "/users/:id",
+            })}
+            {renderOptionalRoute({
+              name: "projects",
+              component: ProjectsPage,
+              exact: true,
+            })}
+            {renderOptionalRoute({
+              name: "projects",
+              component: ProjectDetailsPage,
+              path: "/projects/:id",
+            })}
+            {renderOptionalRoute({ name: "logging", component: LogsPage })}
+            {renderRoute("/streamlog", LogStreamPage)}
             {
               // @ts-ignore
               <Route component={MessagePage} />
@@ -232,8 +265,8 @@ class App extends React.Component<{}, State> {
         <NotificationsModule />
         <Tooltip />
       </Wrapper>
-    )
+    );
   }
 }
 
-export default hot(App)
+export default hot(App);

+ 47 - 39
src/components/Theme.ts

@@ -12,39 +12,39 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import { css } from 'styled-components'
+import { css } from "styled-components";
 
 const exactWidth = (width: string) => css`
   min-width: ${width};
   max-width: ${width};
-`
+`;
 
 const exactHeight = (height: string) => css`
   min-height: ${height};
   max-height: ${height};
-`
+`;
 
 export const ThemePalette = {
-  primary: '#0044CB',
-  primaryLight: '#000EA9',
-  secondary: '#D9DCE3',
-  secondaryLight: '#777A8B',
-  black: '#202134',
-  alert: '#F91661',
-  success: '#4CD964',
-  warning: '#FDC02F',
+  primary: "#0044CB",
+  primaryLight: "#000EA9",
+  secondary: "#D9DCE3",
+  secondaryLight: "#777A8B",
+  black: "#202134",
+  alert: "#F91661",
+  success: "#4CD964",
+  warning: "#FDC02F",
   grayscale: [
-    '#D8DBE2', // 0
-    '#ECEDF1', // 1
-    '#C8CCD7', // 2
-    '#A4AAB5', // 3
-    '#616770', // 4
-    '#7F8795', // 5
-    '#1B2733', // 6
-    '#F2F3F4', // 7
-    '#858B93', // 8
+    "#D8DBE2", // 0
+    "#ECEDF1", // 1
+    "#C8CCD7", // 2
+    "#A4AAB5", // 3
+    "#616770", // 4
+    "#7F8795", // 5
+    "#1B2733", // 6
+    "#F2F3F4", // 7
+    "#858B93", // 8
   ],
-}
+};
 
 export const ThemeProps = {
   fontWeights: {
@@ -61,26 +61,34 @@ export const ThemeProps = {
     wizard: { width: 384 },
   },
 
-  borderRadius: '4px',
-  contentWidth: '928px',
-  boxShadow: 'box-shadow: rgba(0, 0, 0, 0.1) 0 0 6px 2px;',
+  borderRadius: "4px",
+  contentWidth: "928px",
+  boxShadow: "box-shadow: rgba(0, 0, 0, 0.1) 0 0 6px 2px;",
 
   animations: {
-    swift: '.45s cubic-bezier(0.3, 1, 0.4, 1) 0s',
+    swift: ".45s cubic-bezier(0.3, 1, 0.4, 1) 0s",
     rotation: css`
-        animation: rotate 2s infinite linear;
-        @keyframes rotate {
-          from {transform: rotate(0deg);}
-          to {transform: rotate(360deg);}
+      animation: rotate 2s infinite linear;
+      @keyframes rotate {
+        from {
+          transform: rotate(0deg);
         }
-      `,
+        to {
+          transform: rotate(360deg);
+        }
+      }
+    `,
     disabledLoading: css`
-        animation: opacityToggle 750ms linear infinite alternate-reverse;
-        @keyframes opacityToggle {
-          0% {opacity: 1;}
-          100% {opacity: 0.3;}
+      animation: opacityToggle 750ms linear infinite alternate-reverse;
+      @keyframes opacityToggle {
+        0% {
+          opacity: 1;
+        }
+        100% {
+          opacity: 0.3;
         }
-      `,
+      }
+    `,
   },
 
   mobileMaxWidth: 1350,
@@ -88,7 +96,7 @@ export const ThemeProps = {
   exactWidth,
   exactHeight,
   exactSize: (size: string) => css`
-      ${exactWidth(size)}
-      ${exactHeight(size)}
-    `,
-}
+    ${exactWidth(size)}
+    ${exactHeight(size)}
+  `,
+};

+ 52 - 41
src/components/modules/AssessmentModule/AssessedVmListItem/AssessedVmListItem.tsx

@@ -12,20 +12,20 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { observer } from 'mobx-react'
-import styled, { css } from 'styled-components'
+import React from "react";
+import { observer } from "mobx-react";
+import styled, { css } from "styled-components";
 
-import Checkbox from '@src/components/ui/Checkbox'
-import InfoIcon from '@src/components/ui/InfoIcon'
-import DropdownLink from '@src/components/ui/Dropdowns/DropdownLink'
-import type { VmItem } from '@src/@types/Assessment'
-import { ThemePalette } from '@src/components/Theme'
+import Checkbox from "@src/components/ui/Checkbox";
+import InfoIcon from "@src/components/ui/InfoIcon";
+import DropdownLink from "@src/components/ui/Dropdowns/DropdownLink";
+import type { VmItem } from "@src/@types/Assessment";
+import { ThemePalette } from "@src/components/Theme";
 
 const Wrapper = styled.div<any>`
   position: relative;
   width: 100%;
-`
+`;
 const Content = styled.div<any>`
   display: flex;
   margin-left: -32px;
@@ -35,63 +35,68 @@ const Content = styled.div<any>`
   }
 
   opacity: ${(props: any) => (props.disabled ? 0.6 : 1)};
-`
+`;
 const columnWidth = (width: string) => css`
   width: 100%;
   max-width: ${width};
-`
+`;
 const DisplayName = styled.div<any>`
   display: flex;
   ${(props: any) => columnWidth(props.width)}
-`
+`;
 const Value = styled.div<any>`
   color: ${ThemePalette.grayscale[4]};
   ${(props: any) => columnWidth(props.width)}
-`
+`;
 const DisplayNameLabel = styled.div<any>`
   margin-left: 8px;
   word-break: break-word;
-`
+`;
 const InfoIconStyled = styled(InfoIcon)`
   position: absolute;
   left: -36px;
   top: 0px;
   z-index: 10000;
-`
+`;
 
 type Props = {
-  item: VmItem,
-  selected: boolean,
-  onSelectedChange: (item: VmItem, isChecked: boolean) => void,
-  disabled: boolean,
-  loadingVmSizes: boolean,
-  vmSizes: string[],
-  onVmSizeChange: (size: string) => void,
-  selectedVmSize: string | null,
-  recommendedVmSize: string,
-}
+  item: VmItem;
+  selected: boolean;
+  onSelectedChange: (item: VmItem, isChecked: boolean) => void;
+  disabled: boolean;
+  loadingVmSizes: boolean;
+  vmSizes: string[];
+  onVmSizeChange: (size: string) => void;
+  selectedVmSize: string | null;
+  recommendedVmSize: string;
+};
 @observer
 class AssessedVmListItem extends React.Component<Props> {
   renderInfoIcon() {
     if (!this.props.disabled) {
-      return null
+      return null;
     }
 
-    return <InfoIconStyled warning text="We could not detect this VM on the source endpoint. Either the VM is missing or the selected endpoint is not the same as in the Azure Migrare Assesment." />
+    return (
+      <InfoIconStyled
+        warning
+        text="We could not detect this VM on the source endpoint. Either the VM is missing or the selected endpoint is not the same as in the Azure Migrare Assesment."
+      />
+    );
   }
 
   render() {
-    const { disks } = this.props.item.properties
-    let standardCount = 0
-    let premiumCount = 0
+    const { disks } = this.props.item.properties;
+    let standardCount = 0;
+    let premiumCount = 0;
     Object.keys(disks).forEach(diskKey => {
-      if (disks[diskKey].recommendedDiskType === 'Standard') {
-        standardCount += 1
+      if (disks[diskKey].recommendedDiskType === "Standard") {
+        standardCount += 1;
       }
-      if (disks[diskKey].recommendedDiskType === 'Premium') {
-        premiumCount += 1
+      if (disks[diskKey].recommendedDiskType === "Premium") {
+        premiumCount += 1;
       }
-    })
+    });
 
     return (
       <Wrapper>
@@ -100,7 +105,9 @@ class AssessedVmListItem extends React.Component<Props> {
           <DisplayName width="25%">
             <Checkbox
               checked={this.props.selected}
-              onChange={checked => { this.props.onSelectedChange(this.props.item, checked) }}
+              onChange={checked => {
+                this.props.onSelectedChange(this.props.item, checked);
+              }}
               disabled={this.props.disabled}
             />
             <DisplayNameLabel>{`${this.props.item.properties.displayName}`}</DisplayNameLabel>
@@ -115,24 +122,28 @@ class AssessedVmListItem extends React.Component<Props> {
             <DropdownLink
               searchable
               width="208px"
-              noItemsLabel={this.props.loadingVmSizes ? 'Loading...' : 'No data'}
+              noItemsLabel={
+                this.props.loadingVmSizes ? "Loading..." : "No data"
+              }
               selectItemLabel="Auto Determined"
               items={
                 this.props.loadingVmSizes
                   ? []
                   : this.props.vmSizes.map(s => ({ value: s, label: s }))
               }
-              selectedItem={this.props.selectedVmSize || ''}
+              selectedItem={this.props.selectedVmSize || ""}
               listWidth="200px"
-              onChange={item => { this.props.onVmSizeChange(item.value) }}
+              onChange={item => {
+                this.props.onVmSizeChange(item.value);
+              }}
               disabled={this.props.disabled}
               highlightedItem={this.props.recommendedVmSize}
             />
           </Value>
         </Content>
       </Wrapper>
-    )
+    );
   }
 }
 
-export default AssessedVmListItem
+export default AssessedVmListItem;

+ 1 - 2
src/components/modules/AssessmentModule/AssessedVmListItem/package.json

@@ -2,6 +2,5 @@
   "name": "AssessedVmListItem",
   "version": "0.0.0",
   "private": true,
-  "main":"./AssessedVmListItem.tsx"
+  "main": "./AssessedVmListItem.tsx"
 }
-

+ 286 - 186
src/components/modules/AssessmentModule/AssessmentDetailsContent/AssessmentDetailsContent.tsx

@@ -12,34 +12,34 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import styled, { css } from 'styled-components'
-import moment from 'moment'
-import { observer } from 'mobx-react'
-
-import DetailsNavigation from '@src/components/modules/NavigationModule/DetailsNavigation'
-import Button from '@src/components/ui/Button'
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
-import DropdownLink from '@src/components/ui/Dropdowns/DropdownLink'
-import Table from '@src/components/ui/Table'
-import AssessedVmListItem from '@src/components/modules/AssessmentModule/AssessedVmListItem'
-import DropdownFilter from '@src/components/ui/Dropdowns/DropdownFilter'
-import Checkbox from '@src/components/ui/Checkbox'
-import SmallLoading from '@src/components/ui/SmallLoading'
-
-import type { Assessment, VmItem, AzureLocation } from '@src/@types/Assessment'
-import type { Endpoint } from '@src/@types/Endpoint'
-import type { Instance, Nic } from '@src/@types/Instance'
-import type { Network, NetworkMap } from '@src/@types/Network'
-
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import azureMigrateImage from './images/logo.svg'
-import arrowImage from './images/arrow.svg'
+import React from "react";
+import styled, { css } from "styled-components";
+import moment from "moment";
+import { observer } from "mobx-react";
+
+import DetailsNavigation from "@src/components/modules/NavigationModule/DetailsNavigation";
+import Button from "@src/components/ui/Button";
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
+import DropdownLink from "@src/components/ui/Dropdowns/DropdownLink";
+import Table from "@src/components/ui/Table";
+import AssessedVmListItem from "@src/components/modules/AssessmentModule/AssessedVmListItem";
+import DropdownFilter from "@src/components/ui/Dropdowns/DropdownFilter";
+import Checkbox from "@src/components/ui/Checkbox";
+import SmallLoading from "@src/components/ui/SmallLoading";
+
+import type { Assessment, VmItem, AzureLocation } from "@src/@types/Assessment";
+import type { Endpoint } from "@src/@types/Endpoint";
+import type { Instance, Nic } from "@src/@types/Instance";
+import type { Network, NetworkMap } from "@src/@types/Network";
+
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
+import azureMigrateImage from "./images/logo.svg";
+import arrowImage from "./images/arrow.svg";
 
 const Wrapper = styled.div<any>`
   display: flex;
   justify-content: center;
-`
+`;
 const Buttons = styled.div<any>`
   margin-top: 46px;
   display: flex;
@@ -48,193 +48,210 @@ const Buttons = styled.div<any>`
   button:first-child {
     margin-bottom: 16px;
   }
-`
+`;
 const DetailsBody = styled.div<any>`
   ${ThemeProps.exactWidth(ThemeProps.contentWidth)}
   margin-bottom: 32px;
-`
+`;
 const Columns = styled.div<any>`
   display: flex;
   margin-left: -32px;
-`
+`;
 const Column = styled.div<any>`
   width: 50%;
   margin-left: 32px;
-`
+`;
 const Row = styled.div<any>`
   margin-bottom: 32px;
-`
+`;
 const Field = styled.div<any>`
   display: flex;
   flex-direction: column;
-`
+`;
 const Label = styled.div<any>`
   font-size: 10px;
   color: ${ThemePalette.grayscale[3]};
   font-weight: ${ThemeProps.fontWeights.medium};
   text-transform: uppercase;
-`
+`;
 const Value = styled.div<any>`
-  display: ${props => (props.flex ? 'flex' : 'inline-table')};
+  display: ${props => (props.flex ? "flex" : "inline-table")};
   margin-top: 3px;
-  ${props => (props.capitalize ? 'text-transform: capitalize;' : '')}
-`
+  ${props => (props.capitalize ? "text-transform: capitalize;" : "")}
+`;
 const AzureMigrateLogo = styled.div<any>`
   width: 208px;
   height: 32px;
-  background: url('${azureMigrateImage}') center no-repeat;
-`
+  background: url("${azureMigrateImage}") center no-repeat;
+`;
 const LoadingWrapper = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
   margin: 32px 0;
-`
+`;
 const LoadingText = styled.div<any>`
   font-size: 18px;
   margin-top: 32px;
-`
+`;
 const SmallLoadingWrapper = styled.div<any>`
   display: flex;
   align-items: center;
   justify-content: center;
-`
+`;
 const SmallLoadingText = styled.div<any>`
   font-size: 14px;
   margin-left: 16px;
-`
+`;
 const TableStyled = styled(Table)<any>`
   margin-top: 62px;
-  ${props => (props.addWidthPadding ? css`
-    margin-left: -24px;
-    &:after {
-      margin-left: 24px;
-    }
-  ` : '')}
-`
+  ${props =>
+    props.addWidthPadding
+      ? css`
+          margin-left: -24px;
+          &:after {
+            margin-left: 24px;
+          }
+        `
+      : ""}
+`;
 const TableHeaderStyle = css`
   margin-left: 24px;
-`
+`;
 const TableBodyStyle = css`
   padding-left: 24px;
-`
+`;
 const NetworkItem = styled.div<any>`
   display: flex;
   width: 100%;
-`
+`;
 const column = () => css`
   padding-right: 32px;
   width: 100%;
   max-width: 25%;
-`
+`;
 const NetworkName = styled.div<any>`
   ${column()}
-`
+`;
 const Arrow = styled.div<any>`
   width: 32px;
   height: 16px;
   position: absolute;
   right: 0;
-  background: url('${arrowImage}') no-repeat;
+  background: url("${arrowImage}") no-repeat;
   background-position-y: center;
-`
+`;
 const ColumnStub = styled.div<any>`
   ${column()}
   position: relative;
   &:last-child {
     padding-right: 0;
   }
-`
+`;
 const VmHeaderItem = styled.div<any>`
   display: flex;
   font-size: 14px;
-`
+`;
 const VmHeaderItemLabel = styled.div<any>`
   font-size: 10px;
   margin-left: 8px;
-`
+`;
 
 const NavigationItems = [
   {
-    label: 'Details',
-    value: '',
+    label: "Details",
+    value: "",
   },
-]
+];
 
 type Props = {
-  item: Assessment | null,
-  detailsLoading: boolean,
-  instancesDetailsLoading: boolean,
-  instancesLoading: boolean,
-  networksLoading: boolean,
-  instancesDetailsProgress: number | null,
-  targetEndpoint: Endpoint,
-  targetEndpoints: Endpoint[],
-  onTargetEndpointChange: (endpoint: Endpoint) => void,
-  targetEndpointsLoading: boolean,
-  sourceEndpoints: Endpoint[],
-  sourceEndpoint: Endpoint | null,
-  sourceEndpointsLoading: boolean,
-  locations: AzureLocation[],
-  selectedLocation: string | null,
-  onLocationChange: (locationName: string) => void,
-  selectedResourceGroup: string,
-  resourceGroups: string[],
-  onResourceGroupChange: (resourceGroupName: string) => void,
-  targetOptionsLoading: boolean,
-  assessedVmsCount: number,
-  filteredAssessedVms: VmItem[],
-  selectedVms: string[],
-  instancesDetails: Instance[],
-  instances: Instance[],
-  loadingVmSizes: boolean,
-  vmSizes: string[],
-  onVmSizeChange: (vmId: string, size: string) => void,
-  onGetSelectedVmSize: (vm: VmItem) => string | null,
-  networks: Network[],
-  page: string,
-  onSourceEndpointChange: (endpoint: Endpoint) => void,
-  onVmSearchValueChange: (value: string) => void,
-  vmSearchValue: string,
-  onVmSelectedChange: (vm: VmItem, selected: boolean) => void,
-  onNetworkChange: (sourceNic: Nic, targetNetwork: Network) => void,
-  onRefresh: () => void,
-  onMigrateClick: () => void,
-  selectedNetworks: NetworkMap[],
-  selectAllVmsChecked: boolean,
-  onSelectAllVmsChange: (selected: boolean) => void,
-}
+  item: Assessment | null;
+  detailsLoading: boolean;
+  instancesDetailsLoading: boolean;
+  instancesLoading: boolean;
+  networksLoading: boolean;
+  instancesDetailsProgress: number | null;
+  targetEndpoint: Endpoint;
+  targetEndpoints: Endpoint[];
+  onTargetEndpointChange: (endpoint: Endpoint) => void;
+  targetEndpointsLoading: boolean;
+  sourceEndpoints: Endpoint[];
+  sourceEndpoint: Endpoint | null;
+  sourceEndpointsLoading: boolean;
+  locations: AzureLocation[];
+  selectedLocation: string | null;
+  onLocationChange: (locationName: string) => void;
+  selectedResourceGroup: string;
+  resourceGroups: string[];
+  onResourceGroupChange: (resourceGroupName: string) => void;
+  targetOptionsLoading: boolean;
+  assessedVmsCount: number;
+  filteredAssessedVms: VmItem[];
+  selectedVms: string[];
+  instancesDetails: Instance[];
+  instances: Instance[];
+  loadingVmSizes: boolean;
+  vmSizes: string[];
+  onVmSizeChange: (vmId: string, size: string) => void;
+  onGetSelectedVmSize: (vm: VmItem) => string | null;
+  networks: Network[];
+  page: string;
+  onSourceEndpointChange: (endpoint: Endpoint) => void;
+  onVmSearchValueChange: (value: string) => void;
+  vmSearchValue: string;
+  onVmSelectedChange: (vm: VmItem, selected: boolean) => void;
+  onNetworkChange: (sourceNic: Nic, targetNetwork: Network) => void;
+  onRefresh: () => void;
+  onMigrateClick: () => void;
+  selectedNetworks: NetworkMap[];
+  selectAllVmsChecked: boolean;
+  onSelectAllVmsChange: (selected: boolean) => void;
+};
 @observer
 class AssessmentDetailsContent extends React.Component<Props> {
   static defaultProps = {
-    page: '',
-  }
+    page: "",
+  };
 
   doesVmMatchSource(vm: VmItem) {
-    if (!this.props.sourceEndpoint || !this.props.sourceEndpoint.connection_info) {
-      return false
+    if (
+      !this.props.sourceEndpoint ||
+      !this.props.sourceEndpoint.connection_info
+    ) {
+      return false;
     }
 
-    if (this.props.instances.length > 0
-      && !this.props.instances.find(i => i.name === vm.properties.displayName
-        || i.instance_name === vm.properties.displayName)) {
-      return false
+    if (
+      this.props.instances.length > 0 &&
+      !this.props.instances.find(
+        i =>
+          i.name === vm.properties.displayName ||
+          i.instance_name === vm.properties.displayName
+      )
+    ) {
+      return false;
     }
 
-    return this.props.sourceEndpoint.connection_info.host
-      === vm.properties.datacenterManagementServerName
+    return (
+      this.props.sourceEndpoint.connection_info.host ===
+      vm.properties.datacenterManagementServerName
+    );
   }
 
   renderMainDetails() {
-    if (this.props.page !== '' || !this.props.item || !this.props.item.id) {
-      return null
+    if (this.props.page !== "" || !this.props.item || !this.props.item.id) {
+      return null;
     }
 
     const status = this.props.item
-      ? this.props.item.properties.status === 'Completed' ? 'Ready for Migration' : this.props.item.properties.status : ''
+      ? this.props.item.properties.status === "Completed"
+        ? "Ready for Migration"
+        : this.props.item.properties.status
+      : "";
 
-    const locationItem: AzureLocation | undefined = this.props.locations
-      .find(l => l.id === this.props.selectedLocation)
+    const locationItem: AzureLocation | undefined = this.props.locations.find(
+      l => l.id === this.props.selectedLocation
+    );
 
     return (
       <Columns>
@@ -246,20 +263,26 @@ class AssessmentDetailsContent extends React.Component<Props> {
             <Field>
               <Label>Last Update</Label>
               <Value>
-                {this.props.item ? moment(this.props.item.properties.updatedTimestamp).format('YYYY-MM-DD HH:mm:ss') : '-'}
+                {this.props.item
+                  ? moment(this.props.item.properties.updatedTimestamp).format(
+                      "YYYY-MM-DD HH:mm:ss"
+                    )
+                  : "-"}
               </Value>
             </Field>
           </Row>
           <Row>
             <Field>
               <Label>Migration Project</Label>
-              <Value>{this.props.item ? this.props.item.projectName : ''}</Value>
+              <Value>
+                {this.props.item ? this.props.item.projectName : ""}
+              </Value>
             </Field>
           </Row>
           <Row>
             <Field>
               <Label>VM Group</Label>
-              <Value>{this.props.item ? this.props.item.groupName : ''}</Value>
+              <Value>{this.props.item ? this.props.item.groupName : ""}</Value>
             </Field>
           </Row>
           <Row>
@@ -275,13 +298,25 @@ class AssessmentDetailsContent extends React.Component<Props> {
               <Label>Source Endpoint</Label>
               <Value>
                 <DropdownLink
-                  selectedItem={this.props.sourceEndpoint ? this.props.sourceEndpoint.id : ''}
+                  selectedItem={
+                    this.props.sourceEndpoint
+                      ? this.props.sourceEndpoint.id
+                      : ""
+                  }
                   items={this.props.sourceEndpoints.map(endpoint => ({
-                    label: endpoint.name, value: endpoint.id, endpoint,
+                    label: endpoint.name,
+                    value: endpoint.id,
+                    endpoint,
                   }))}
-                  onChange={item => { this.props.onSourceEndpointChange(item.endpoint) }}
+                  onChange={item => {
+                    this.props.onSourceEndpointChange(item.endpoint);
+                  }}
                   selectItemLabel="Select Endpoint"
-                  noItemsLabel={this.props.sourceEndpointsLoading ? 'Loading ....' : 'No matching endpoints'}
+                  noItemsLabel={
+                    this.props.sourceEndpointsLoading
+                      ? "Loading ...."
+                      : "No matching endpoints"
+                  }
                 />
               </Value>
             </Field>
@@ -291,13 +326,25 @@ class AssessmentDetailsContent extends React.Component<Props> {
               <Label>Target endpoint</Label>
               <Value>
                 <DropdownLink
-                  selectedItem={this.props.targetEndpoint ? this.props.targetEndpoint.id : ''}
+                  selectedItem={
+                    this.props.targetEndpoint
+                      ? this.props.targetEndpoint.id
+                      : ""
+                  }
                   items={this.props.targetEndpoints.map(endpoint => ({
-                    label: endpoint.name, value: endpoint.id, endpoint,
+                    label: endpoint.name,
+                    value: endpoint.id,
+                    endpoint,
                   }))}
-                  onChange={item => { this.props.onTargetEndpointChange(item.endpoint) }}
+                  onChange={item => {
+                    this.props.onTargetEndpointChange(item.endpoint);
+                  }}
                   selectItemLabel="Select Endpoint"
-                  noItemsLabel={this.props.targetEndpointsLoading ? 'Loading ....' : 'No Azure endpoints'}
+                  noItemsLabel={
+                    this.props.targetEndpointsLoading
+                      ? "Loading ...."
+                      : "No Azure endpoints"
+                  }
                 />
               </Value>
             </Field>
@@ -308,9 +355,18 @@ class AssessmentDetailsContent extends React.Component<Props> {
               <Value>
                 <DropdownLink
                   selectedItem={this.props.selectedResourceGroup}
-                  items={this.props.resourceGroups.map(group => ({ label: group, value: group }))}
-                  onChange={item => { this.props.onResourceGroupChange(item.value) }}
-                  noItemsLabel={this.props.targetOptionsLoading ? 'Loading ....' : 'No Resource Groups found'}
+                  items={this.props.resourceGroups.map(group => ({
+                    label: group,
+                    value: group,
+                  }))}
+                  onChange={item => {
+                    this.props.onResourceGroupChange(item.value);
+                  }}
+                  noItemsLabel={
+                    this.props.targetOptionsLoading
+                      ? "Loading ...."
+                      : "No Resource Groups found"
+                  }
                 />
               </Value>
             </Field>
@@ -320,58 +376,78 @@ class AssessmentDetailsContent extends React.Component<Props> {
               <Label>Location</Label>
               <Value>
                 <DropdownLink
-                  selectedItem={locationItem ? locationItem.id : ''}
+                  selectedItem={locationItem ? locationItem.id : ""}
                   items={this.props.locations.map(location => ({
-                    label: location.name, value: location.id,
+                    label: location.name,
+                    value: location.id,
                   }))}
-                  onChange={item => { this.props.onLocationChange(item.value) }}
-                  noItemsLabel={this.props.targetOptionsLoading ? 'Loading ....' : 'No Locations found'}
+                  onChange={item => {
+                    this.props.onLocationChange(item.value);
+                  }}
+                  noItemsLabel={
+                    this.props.targetOptionsLoading
+                      ? "Loading ...."
+                      : "No Locations found"
+                  }
                 />
               </Value>
             </Field>
           </Row>
         </Column>
       </Columns>
-    )
+    );
   }
 
   renderVmsTable() {
-    const loading = this.props.instancesLoading
+    const loading = this.props.instancesLoading;
 
     const items = this.props.filteredAssessedVms.map(vm => (
       <AssessedVmListItem
+        key={vm.id}
         item={vm}
-        selected={this.props.selectedVms.filter(m => m === vm.properties.displayName).length > 0}
+        selected={
+          this.props.selectedVms.filter(m => m === vm.properties.displayName)
+            .length > 0
+        }
         onSelectedChange={(selectedVm, selected) => {
-          this.props.onVmSelectedChange(selectedVm, selected)
+          this.props.onVmSelectedChange(selectedVm, selected);
         }}
         disabled={!this.doesVmMatchSource(vm)}
         loadingVmSizes={this.props.loadingVmSizes}
         recommendedVmSize={vm.properties.recommendedSize}
         vmSizes={this.props.vmSizes}
         selectedVmSize={this.props.onGetSelectedVmSize(vm)}
-        onVmSizeChange={size => { this.props.onVmSizeChange(vm.properties.displayName, size) }}
+        onVmSizeChange={size => {
+          this.props.onVmSizeChange(vm.properties.displayName, size);
+        }}
       />
-    ))
+    ));
 
-    const vmCountLabel = `(${this.props.filteredAssessedVms.length === this.props.assessedVmsCount ? this.props.assessedVmsCount
-      : `${this.props.filteredAssessedVms.length} OUT OF ${this.props.assessedVmsCount}`})`
+    const vmCountLabel = `(${
+      this.props.filteredAssessedVms.length === this.props.assessedVmsCount
+        ? this.props.assessedVmsCount
+        : `${this.props.filteredAssessedVms.length} OUT OF ${this.props.assessedVmsCount}`
+    })`;
     const vmHeaderItem = (
       <VmHeaderItem>
         {loading ? null : (
           <Checkbox
             checked={this.props.selectAllVmsChecked}
-            onChange={checked => { this.props.onSelectAllVmsChange(checked) }}
+            onChange={checked => {
+              this.props.onSelectAllVmsChange(checked);
+            }}
           />
         )}
         <VmHeaderItemLabel>Virtual Machine {vmCountLabel}</VmHeaderItemLabel>
         <DropdownFilter
           searchPlaceholder="Filter Virtual Machines"
           searchValue={this.props.vmSearchValue}
-          onSearchChange={value => { this.props.onVmSearchValueChange(value) }}
+          onSearchChange={value => {
+            this.props.onVmSearchValueChange(value);
+          }}
         />
       </VmHeaderItem>
-    )
+    );
 
     return (
       <TableStyled
@@ -379,58 +455,65 @@ class AssessmentDetailsContent extends React.Component<Props> {
         items={loading ? [] : items}
         bodyStyle={TableBodyStyle}
         headerStyle={TableHeaderStyle}
-        header={[vmHeaderItem, 'OS', 'Target Disk Type', 'Azure VM Size']}
+        header={[vmHeaderItem, "OS", "Target Disk Type", "Azure VM Size"]}
         useSecondaryStyle
-        noItemsComponent={this.renderLoading('Loading instances, please wait ...')}
+        noItemsComponent={this.renderLoading(
+          "Loading instances, please wait ..."
+        )}
       />
-    )
+    );
   }
 
   renderNetworkTable() {
-    const loading = this.props.networksLoading || this.props.instancesDetailsLoading
+    const loading =
+      this.props.networksLoading || this.props.instancesDetailsLoading;
 
     if (loading) {
       return (
         <TableStyled
           items={[]}
-          header={['Source Network', '', '', 'Target Network']}
+          header={["Source Network", "", "", "Target Network"]}
           useSecondaryStyle
           noItemsStyle={{ marginLeft: 0 }}
           noItemsComponent={this.renderNetworksLoading()}
         />
-      )
+      );
     }
 
-    const nics: Nic[] = []
+    const nics: Nic[] = [];
     this.props.instancesDetails.forEach(instance => {
       if (!instance.devices || !instance.devices.nics) {
-        return
+        return;
       }
       instance.devices.nics.forEach(nic => {
         if (nics.find(n => n.network_name === nic.network_name)) {
-          return
+          return;
         }
-        nics.push(nic)
-      })
-    })
+        nics.push(nic);
+      });
+    });
 
     if (nics.length === 0) {
-      return null
+      return null;
     }
 
     const items = nics.map(nic => {
-      let selectedNetworkName: string | undefined
-      const selectedNetwork = this.props.selectedNetworks
-        && this.props.selectedNetworks.find(n => n.sourceNic.network_name === nic.network_name)
+      let selectedNetworkName: string | undefined;
+      const selectedNetwork =
+        this.props.selectedNetworks &&
+        this.props.selectedNetworks.find(
+          n => n.sourceNic.network_name === nic.network_name
+        );
       if (selectedNetwork) {
-        selectedNetworkName = selectedNetwork.targetNetwork?.name
+        selectedNetworkName = selectedNetwork.targetNetwork?.name;
       }
 
       return (
-
         <NetworkItem key={nic.network_name}>
           <NetworkName width="25%">{nic.network_name}</NetworkName>
-          <ColumnStub width="25%"><Arrow /></ColumnStub>
+          <ColumnStub width="25%">
+            <Arrow />
+          </ColumnStub>
           <ColumnStub width="25%" />
           <ColumnStub width="25%">
             <DropdownLink
@@ -438,29 +521,35 @@ class AssessmentDetailsContent extends React.Component<Props> {
               noItemsLabel="No Networks found"
               selectItemLabel="Select Network"
               selectedItem={selectedNetworkName}
-              onChange={item => { this.props.onNetworkChange(nic, item.network) }}
-              items={this.props.networks.map(network => ({ value: network.name || '', label: network.name || '', network }))}
+              onChange={item => {
+                this.props.onNetworkChange(nic, item.network);
+              }}
+              items={this.props.networks.map(network => ({
+                value: network.name || "",
+                label: network.name || "",
+                network,
+              }))}
             />
           </ColumnStub>
         </NetworkItem>
-      )
-    })
+      );
+    });
     return (
       <TableStyled
         items={loading ? [] : items}
-        header={['Source Network', '', '', 'Target Network']}
+        header={["Source Network", "", "", "Target Network"]}
         useSecondaryStyle
         noItemsStyle={{ marginLeft: 0 }}
         noItemsComponent={this.renderNetworksLoading()}
       />
-    )
+    );
   }
 
   renderNetworksLoading() {
-    let loadingProgress = -1
+    let loadingProgress = -1;
     if (this.props.instancesDetailsLoading) {
       if (this.props.instancesDetailsProgress != null) {
-        loadingProgress = Math.round(this.props.instancesDetailsProgress * 100)
+        loadingProgress = Math.round(this.props.instancesDetailsProgress * 100);
       }
     }
 
@@ -469,20 +558,28 @@ class AssessmentDetailsContent extends React.Component<Props> {
         <SmallLoading loadingProgress={loadingProgress} />
         <SmallLoadingText>Loading networks, please wait ...</SmallLoadingText>
       </SmallLoadingWrapper>
-    )
+    );
   }
 
   renderButtons() {
     return (
       <Buttons>
-        <Button secondary onClick={this.props.onRefresh}>Refresh</Button>
+        <Button secondary onClick={this.props.onRefresh}>
+          Refresh
+        </Button>
         <Button
-          disabled={this.props.selectedVms.length === 0 || this.props.selectedNetworks.length === 0}
-          onClick={() => { this.props.onMigrateClick() }}
-        >Migrate / Replicate
+          disabled={
+            this.props.selectedVms.length === 0 ||
+            this.props.selectedNetworks.length === 0
+          }
+          onClick={() => {
+            this.props.onMigrateClick();
+          }}
+        >
+          Migrate / Replicate
         </Button>
       </Buttons>
-    )
+    );
   }
 
   renderLoading(message: string) {
@@ -491,7 +588,7 @@ class AssessmentDetailsContent extends React.Component<Props> {
         <StatusImage loading />
         <LoadingText>{message}</LoadingText>
       </LoadingWrapper>
-    )
+    );
   }
 
   render() {
@@ -500,20 +597,23 @@ class AssessmentDetailsContent extends React.Component<Props> {
         <DetailsNavigation
           items={NavigationItems}
           selectedValue={this.props.page}
-          itemId={this.props.item ? this.props.item.id : ''}
-          customHref={() => '#'}
+          itemId={this.props.item ? this.props.item.id : ""}
+          customHref={() => "#"}
         />
         <DetailsBody>
           {this.props.detailsLoading ? null : this.renderMainDetails()}
-          {this.props.detailsLoading ? this.renderLoading('Loading assessment...') : null}
+          {this.props.detailsLoading
+            ? this.renderLoading("Loading assessment...")
+            : null}
           {this.props.detailsLoading ? null : this.renderVmsTable()}
           {this.props.detailsLoading || this.props.instancesLoading
-            ? null : this.renderNetworkTable()}
+            ? null
+            : this.renderNetworkTable()}
           {this.props.detailsLoading ? null : this.renderButtons()}
         </DetailsBody>
       </Wrapper>
-    )
+    );
   }
 }
 
-export default AssessmentDetailsContent
+export default AssessmentDetailsContent;

+ 1 - 2
src/components/modules/AssessmentModule/AssessmentDetailsContent/package.json

@@ -2,6 +2,5 @@
   "name": "AssessmentDetailsContent",
   "version": "0.0.0",
   "private": true,
-  "main":"./AssessmentDetailsContent.tsx"
+  "main": "./AssessmentDetailsContent.tsx"
 }
-

+ 39 - 43
src/components/modules/AssessmentModule/AssessmentListItem/AssessmentListItem.tsx

@@ -12,17 +12,17 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
+import React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
 
-import StatusPill from '@src/components/ui/StatusComponents/StatusPill'
+import StatusPill from "@src/components/ui/StatusComponents/StatusPill";
 
-import type { Assessment } from '@src/@types/Assessment'
+import type { Assessment } from "@src/@types/Assessment";
 
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import assessmentImage from './images/assessment.svg'
-import azureMigrateImage from './images/azure-migrate.svg'
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
+import assessmentImage from "./images/assessment.svg";
+import azureMigrateImage from "./images/azure-migrate.svg";
 
 const Content = styled.div<any>`
   display: flex;
@@ -37,7 +37,7 @@ const Content = styled.div<any>`
   &:hover {
     background: ${ThemePalette.grayscale[1]};
   }
-`
+`;
 const Wrapper = styled.div<any>`
   display: flex;
   align-items: center;
@@ -45,72 +45,72 @@ const Wrapper = styled.div<any>`
   &:last-child ${Content} {
     border-bottom: 1px solid ${ThemePalette.grayscale[1]};
   }
-`
+`;
 const Image = styled.div<any>`
   min-width: 48px;
   height: 48px;
-  background: url('${(props: any) => props.image}') no-repeat center;
+  background: url("${(props: any) => props.image}") no-repeat center;
   margin-right: 16px;
-`
+`;
 const Title = styled.div<any>`
   flex-grow: 1;
   overflow: hidden;
   margin-right: 48px;
   min-width: 100px;
-`
+`;
 const TitleLabel = styled.div<any>`
   font-size: 16px;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-`
+`;
 const AssessmentType = styled.div<any>`
   display: flex;
   justify-content: center;
   align-items: center;
   margin-right: 46px;
-  ${ThemeProps.exactWidth('180px')}
-`
+  ${ThemeProps.exactWidth("180px")}
+`;
 const AssessmentImage = styled.div<any>`
   width: 48px;
   height: 32px;
-  background: url('${azureMigrateImage}') center no-repeat;
+  background: url("${azureMigrateImage}") center no-repeat;
   margin-right: 12px;
-`
+`;
 const AssessmentLabel = styled.div<any>`
   font-size: 15px;
   color: ${ThemePalette.grayscale[4]};
   width: 64px;
-`
+`;
 const TotalVms = styled.div<any>`
-  ${ThemeProps.exactWidth('96px')}
+  ${ThemeProps.exactWidth("96px")}
   margin-right: 48px;
-`
+`;
 const Project = styled.div<any>`
-  ${ThemeProps.exactWidth('175px')}
+  ${ThemeProps.exactWidth("175px")}
   margin-right: 48px;
-`
+`;
 const ItemLabel = styled.div<any>`
   color: ${ThemePalette.grayscale[4]};
-`
+`;
 const ItemValue = styled.div<any>`
   color: ${ThemePalette.primary};
-`
+`;
 
 type Props = {
-  item: Assessment,
-  onClick: () => void,
-}
+  item: Assessment;
+  onClick: () => void;
+};
 @observer
 class AssessmentListItem extends React.Component<Props> {
   render() {
-    let status = this.props.item.properties.status.toUpperCase()
-    let label = status
-    if (status === 'CREATED' || status === 'RUNNING') {
-      status = 'RUNNING'
-      label = 'CREATING'
-    } else if (status === 'COMPLETED') {
-      label = 'READY'
+    let status = this.props.item.properties.status.toUpperCase();
+    let label = status;
+    if (status === "CREATED" || status === "RUNNING") {
+      status = "RUNNING";
+      label = "CREATING";
+    } else if (status === "COMPLETED") {
+      label = "READY";
     }
 
     return (
@@ -127,20 +127,16 @@ class AssessmentListItem extends React.Component<Props> {
           </AssessmentType>
           <Project>
             <ItemLabel>Project</ItemLabel>
-            <ItemValue>
-              {this.props.item.project.name}
-            </ItemValue>
+            <ItemValue>{this.props.item.project.name}</ItemValue>
           </Project>
           <TotalVms>
             <ItemLabel>Instances</ItemLabel>
-            <ItemValue>
-              {this.props.item.properties.numberOfMachines}
-            </ItemValue>
+            <ItemValue>{this.props.item.properties.numberOfMachines}</ItemValue>
           </TotalVms>
         </Content>
       </Wrapper>
-    )
+    );
   }
 }
 
-export default AssessmentListItem
+export default AssessmentListItem;

+ 1 - 2
src/components/modules/AssessmentModule/AssessmentListItem/package.json

@@ -2,6 +2,5 @@
   "name": "AssessmentListItem",
   "version": "0.0.0",
   "private": true,
-  "main":"./AssessmentListItem.tsx"
+  "main": "./AssessmentListItem.tsx"
 }
-

+ 136 - 117
src/components/modules/AssessmentModule/AssessmentMigrationOptions/AssessmentMigrationOptions.tsx

@@ -12,21 +12,21 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import * as React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
+import * as React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
 
-import Button from '@src/components/ui/Button'
-import FieldInput from '@src/components/ui/FieldInput'
-import ToggleButtonBar from '@src/components/ui/ToggleButtonBar'
+import Button from "@src/components/ui/Button";
+import FieldInput from "@src/components/ui/FieldInput";
+import ToggleButtonBar from "@src/components/ui/ToggleButtonBar";
 
-import type { Field } from '@src/@types/Field'
+import type { Field } from "@src/@types/Field";
 
-import { ThemeProps } from '@src/components/Theme'
+import { ThemeProps } from "@src/components/Theme";
 
-import LabelDictionary from '@src/utils/LabelDictionary'
+import LabelDictionary from "@src/utils/LabelDictionary";
 
-import assessmentImage from './images/assessment.svg'
+import assessmentImage from "./images/assessment.svg";
 
 const Wrapper = styled.div<any>`
   padding: 48px 32px 32px 32px;
@@ -34,15 +34,15 @@ const Wrapper = styled.div<any>`
   flex-direction: column;
   align-items: center;
   min-height: 0;
-`
+`;
 const Image = styled.div<any>`
   width: 96px;
   height: 96px;
-  background: url('${assessmentImage}') center no-repeat;
-`
+  background: url("${assessmentImage}") center no-repeat;
+`;
 const ToggleButtonBarStyled = styled(ToggleButtonBar)`
   margin-top: 48px;
-`
+`;
 const Fields = styled.div<any>`
   display: flex;
   margin-top: 32px;
@@ -50,59 +50,59 @@ const Fields = styled.div<any>`
   overflow: auto;
   width: 100%;
   min-height: 0;
-`
+`;
 const FieldStyled = styled(FieldInput)`
   ${ThemeProps.exactWidth(`${ThemeProps.inputSizes.large.width}px`)}
   margin-bottom: 16px;
-`
+`;
 const Row = styled.div<any>`
   display: flex;
   flex-shrink: 0;
   justify-content: space-between;
-`
+`;
 
 const Buttons = styled.div<any>`
   display: flex;
   justify-content: space-between;
   width: 100%;
   margin-top: 32px;
-`
+`;
 
 const generalFields = [
   {
-    name: 'use_replica',
-    type: 'boolean',
+    name: "use_replica",
+    type: "boolean",
   },
   {
-    name: 'separate_vm',
-    type: 'boolean',
+    name: "separate_vm",
+    type: "boolean",
   },
-]
+];
 const replicaFields = [
   {
-    name: 'shutdown_instances',
-    type: 'boolean',
+    name: "shutdown_instances",
+    type: "boolean",
   },
-]
+];
 const migrationFields = [
   {
-    name: 'skip_os_morphing',
-    type: 'boolean',
+    name: "skip_os_morphing",
+    type: "boolean",
   },
-]
+];
 
 type Props = {
-  onCancelClick: () => void,
-  onExecuteClick: (fieldValues: { [prop: string]: any }) => void,
-  executeButtonDisabled: boolean,
-  replicaSchema: Field[],
-  migrationSchema: Field[],
-  onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
-}
+  onCancelClick: () => void;
+  onExecuteClick: (fieldValues: { [prop: string]: any }) => void;
+  executeButtonDisabled: boolean;
+  replicaSchema: Field[];
+  migrationSchema: Field[];
+  onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void;
+};
 type State = {
-  fieldValues: { [prop: string]: any },
-  showAdvancedOptions: boolean,
-}
+  fieldValues: { [prop: string]: any };
+  showAdvancedOptions: boolean;
+};
 @observer
 class AssessmentMigrationOptions extends React.Component<Props, State> {
   state: State = {
@@ -113,31 +113,34 @@ class AssessmentMigrationOptions extends React.Component<Props, State> {
       skip_os_morphing: false,
     },
     showAdvancedOptions: false,
-  }
+  };
 
   // scrollableRef: HTMLElement | undefined | null
 
   getFieldValue(fieldName: string) {
     if (this.state.fieldValues[fieldName] != null) {
-      return this.state.fieldValues[fieldName]
+      return this.state.fieldValues[fieldName];
     }
-    return null
+    return null;
   }
 
   getObjectFieldValue(fieldName: string, propName: string) {
-    return this.state.fieldValues[fieldName] && this.state.fieldValues[fieldName][propName]
+    return (
+      this.state.fieldValues[fieldName] &&
+      this.state.fieldValues[fieldName][propName]
+    );
   }
 
   handleValueChange(fieldName: string, value: any) {
     this.setState(prevState => {
-      const fieldValues = { ...prevState.fieldValues }
+      const fieldValues = { ...prevState.fieldValues };
       if (value != null) {
-        fieldValues[fieldName] = value
+        fieldValues[fieldName] = value;
       } else {
-        delete fieldValues[fieldName]
+        delete fieldValues[fieldName];
       }
-      return { fieldValues }
-    })
+      return { fieldValues };
+    });
   }
 
   // UNSAFE_componentDidUpdate(_: Props, prevState: State) {
@@ -149,46 +152,47 @@ class AssessmentMigrationOptions extends React.Component<Props, State> {
 
   handleObjectValueChange(fieldName: string, propName: string, value: any) {
     this.setState(prevState => {
-      const fieldValues = { ...prevState.fieldValues }
+      const fieldValues = { ...prevState.fieldValues };
       if (!fieldValues[fieldName]) {
-        fieldValues[fieldName] = {}
+        fieldValues[fieldName] = {};
       }
-      fieldValues[fieldName][propName] = value
-      return { fieldValues }
-    })
+      fieldValues[fieldName][propName] = value;
+      return { fieldValues };
+    });
   }
 
   renderFields() {
-    let fields: any = generalFields
-    const useReplica = this.getFieldValue('use_replica')
-    const skipFields = ['location', 'resource_group', 'network_map', 'storage_map', 'vm_size', 'worker_size']
+    let fields: any = generalFields;
+    const useReplica = this.getFieldValue("use_replica");
+    const skipFields = [
+      "location",
+      "resource_group",
+      "network_map",
+      "storage_map",
+      "vm_size",
+      "worker_size",
+    ];
     // eslint-disable-next-line no-shadow
-    const cleanup = (cleanupFields: any[]) => cleanupFields.filter((f: {
-      name: string
-    }) => !skipFields
-      .find(n => n === f.name)).map((f: { type: string; nullableBoolean: boolean }) => {
-      if (f.type === 'boolean') {
-        // eslint-disable-next-line no-param-reassign
-        f.nullableBoolean = true
-      }
-      return { ...f }
-    })
+    const cleanup = (cleanupFields: any[]) =>
+      cleanupFields
+        .filter((f: { name: string }) => !skipFields.find(n => n === f.name))
+        .map((f: { type: string; nullableBoolean: boolean }) => {
+          if (f.type === "boolean") {
+            // eslint-disable-next-line no-param-reassign
+            f.nullableBoolean = true;
+          }
+          return { ...f };
+        });
 
     if (useReplica) {
-      fields = [...fields, ...replicaFields]
+      fields = [...fields, ...replicaFields];
       if (this.state.showAdvancedOptions) {
-        fields = [
-          ...fields,
-          ...cleanup(this.props.replicaSchema),
-        ]
+        fields = [...fields, ...cleanup(this.props.replicaSchema)];
       }
     } else {
-      fields = [...fields, ...migrationFields]
+      fields = [...fields, ...migrationFields];
       if (this.state.showAdvancedOptions) {
-        fields = [
-          ...fields,
-          ...cleanup(this.props.migrationSchema),
-        ]
+        fields = [...fields, ...cleanup(this.props.migrationSchema)];
       }
     }
 
@@ -196,43 +200,49 @@ class AssessmentMigrationOptions extends React.Component<Props, State> {
       boolean: 1,
       string: 2,
       object: 3,
-    }
+    };
     fields.sort((a: any, b: any) => {
       if (sortPriority[a.type] && sortPriority[b.type]) {
-        return sortPriority[a.type] - sortPriority[b.type]
+        return sortPriority[a.type] - sortPriority[b.type];
       }
       if (sortPriority[a.type]) {
-        return -1
+        return -1;
       }
       if (sortPriority[b.type]) {
-        return 1
+        return 1;
       }
-      return a.name.localeCompare(b.name)
-    })
+      return a.name.localeCompare(b.name);
+    });
 
-    const rows: JSX.Element[] = []
-    let lastField: JSX.Element
+    const rows: JSX.Element[] = [];
+    let lastField: JSX.Element;
     fields.forEach((field: any, index: number) => {
-      let additionalProps
-      if (field.type === 'object' && field.properties) {
+      let additionalProps;
+      if (field.type === "object" && field.properties) {
         additionalProps = {
-          valueCallback: (
-            callbackField: { name: string },
-          ) => this.getObjectFieldValue(field.name, callbackField.name),
+          valueCallback: (callbackField: { name: string }) =>
+            this.getObjectFieldValue(field.name, callbackField.name),
           onChange: (value: any, callbackField: { name: string }) => {
-            const propName = callbackField.name.substr(callbackField.name.lastIndexOf('/') + 1)
-            this.handleObjectValueChange(field.name, propName, value)
+            const propName = callbackField.name.substr(
+              callbackField.name.lastIndexOf("/") + 1
+            );
+            this.handleObjectValueChange(field.name, propName, value);
           },
-          properties: field.properties.map((p: any) => ({ ...p, required: false })),
-        }
+          properties: field.properties.map((p: any) => ({
+            ...p,
+            required: false,
+          })),
+        };
       } else {
-        const value = this.getFieldValue(field.name)
+        const value = this.getFieldValue(field.name);
         additionalProps = {
           value,
           // eslint-disable-next-line no-shadow
-          onChange: (changeValue: any) => { this.handleValueChange(field.name, changeValue) },
+          onChange: (changeValue: any) => {
+            this.handleValueChange(field.name, changeValue);
+          },
           type: field.type,
-        }
+        };
       }
 
       const currentField = (
@@ -244,30 +254,28 @@ class AssessmentMigrationOptions extends React.Component<Props, State> {
           {...additionalProps}
           label={field.label || LabelDictionary.get(field.name)}
         />
-      )
+      );
       const pushRow = (field1: React.ReactNode, field2?: React.ReactNode) => {
-        rows.push((
+        rows.push(
           <Row key={field.name}>
             {field1}
             {field2}
           </Row>
-        ))
-      }
+        );
+      };
       if (index === fields.length - 1 && index % 2 === 0) {
-        pushRow(currentField)
+        pushRow(currentField);
       } else if (index % 2 !== 0) {
-        pushRow(lastField, currentField)
+        pushRow(lastField, currentField);
       } else {
-        lastField = currentField
+        lastField = currentField;
       }
-    })
+    });
 
     return (
       // <Fields ref={(ref: HTMLElement | null | undefined) => { this.scrollableRef = ref }}>
-      <Fields>
-        {rows}
-      </Fields>
-    )
+      <Fields>{rows}</Fields>
+    );
   }
 
   render() {
@@ -275,28 +283,39 @@ class AssessmentMigrationOptions extends React.Component<Props, State> {
       <Wrapper>
         <Image />
         <ToggleButtonBarStyled
-          items={[{ label: 'Basic', value: 'basic' }, { label: 'Advanced', value: 'advanced' }]}
-          selectedValue={this.state.showAdvancedOptions ? 'advanced' : 'basic'}
-          onChange={item => { this.setState({ showAdvancedOptions: item.value === 'advanced' }) }}
+          items={[
+            { label: "Basic", value: "basic" },
+            { label: "Advanced", value: "advanced" },
+          ]}
+          selectedValue={this.state.showAdvancedOptions ? "advanced" : "basic"}
+          onChange={item => {
+            this.setState({ showAdvancedOptions: item.value === "advanced" });
+          }}
         />
         {this.renderFields()}
         <Buttons>
           <Button
             large
             secondary
-            onClick={() => { this.props.onCancelClick() }}
-          >Cancel
+            onClick={() => {
+              this.props.onCancelClick();
+            }}
+          >
+            Cancel
           </Button>
           <Button
             large
-            onClick={() => { this.props.onExecuteClick(this.state.fieldValues) }}
+            onClick={() => {
+              this.props.onExecuteClick(this.state.fieldValues);
+            }}
             disabled={this.props.executeButtonDisabled}
-          >Execute
+          >
+            Execute
           </Button>
         </Buttons>
       </Wrapper>
-    )
+    );
   }
 }
 
-export default AssessmentMigrationOptions
+export default AssessmentMigrationOptions;

+ 1 - 2
src/components/modules/AssessmentModule/AssessmentMigrationOptions/package.json

@@ -2,6 +2,5 @@
   "name": "AssessmentMigrationOptions",
   "version": "0.0.0",
   "private": true,
-  "main":"./AssessmentMigrationOptions.tsx"
+  "main": "./AssessmentMigrationOptions.tsx"
 }
-

+ 80 - 63
src/components/modules/DashboardModule/DashboardActivity/DashboardActivity.spec.tsx

@@ -12,88 +12,105 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { render } from '@testing-library/react'
-import TestUtils from '@tests/TestUtils'
-import { NotificationItemData } from '@src/@types/NotificationItem'
-import progressImage from '@src/components/ui/StatusComponents/StatusIcon/images/progress'
-import { ThemePalette } from '@src/components/Theme'
-import DashboardActivity from '.'
+import React from "react";
+import { render } from "@testing-library/react";
+import TestUtils from "@tests/TestUtils";
+import { NotificationItemData } from "@src/@types/NotificationItem";
+import progressImage from "@src/components/ui/StatusComponents/StatusIcon/images/progress";
+import { ThemePalette } from "@src/components/Theme";
+import DashboardActivity from ".";
 
-const encodedProgressImage = encodeURIComponent(progressImage(ThemePalette.grayscale[3], ThemePalette.primary))
+const encodedProgressImage = encodeURIComponent(
+  progressImage(ThemePalette.grayscale[3], ThemePalette.primary)
+);
 
-jest.mock('react-router-dom', () => ({ Link: 'a' }))
+jest.mock("react-router-dom", () => ({ Link: "a" }));
 
 const ITEMS: NotificationItemData[] = [
   {
-    id: '1',
-    type: 'replica',
-    status: 'ERROR',
-    name: 'Replica 1',
-    description: 'Replica 1 description',
+    id: "1",
+    type: "replica",
+    status: "ERROR",
+    name: "Replica 1",
+    description: "Replica 1 description",
   },
   {
-    id: '2',
-    type: 'migration',
-    status: 'RUNNING',
-    name: 'Migration 1',
-    description: 'Migration 1 description',
+    id: "2",
+    type: "migration",
+    status: "RUNNING",
+    name: "Migration 1",
+    description: "Migration 1 description",
   },
   {
-    id: '3',
-    type: 'migration',
-    status: 'COMPLETED',
-    name: 'Migration 2',
-    description: 'Migration 2 description',
+    id: "3",
+    type: "migration",
+    status: "COMPLETED",
+    name: "Migration 2",
+    description: "Migration 2 description",
   },
-]
+];
 
-describe('DashboardActivity', () => {
-  it('renders no recent activity', () => {
-    render(<DashboardActivity notificationItems={[]} />)
-    expect(TestUtils.select('DashboardActivity__Message')!.textContent).toContain('There is no recent activity')
-  })
+describe("DashboardActivity", () => {
+  it("renders no recent activity", () => {
+    render(<DashboardActivity notificationItems={[]} />);
+    expect(
+      TestUtils.select("DashboardActivity__Message")!.textContent
+    ).toContain("There is no recent activity");
+  });
 
-  it('fires new click', () => {
-    const onNewClick = jest.fn()
-    render(<DashboardActivity notificationItems={[]} onNewClick={onNewClick} />)
-    TestUtils.select('Button__StyledButton')!.click()
-    expect(onNewClick).toHaveBeenCalled()
-  })
+  it("fires new click", () => {
+    const onNewClick = jest.fn();
+    render(
+      <DashboardActivity notificationItems={[]} onNewClick={onNewClick} />
+    );
+    TestUtils.select("Button__StyledButton")!.click();
+    expect(onNewClick).toHaveBeenCalled();
+  });
 
-  it('renders loading', () => {
-    const { rerender } = render(<DashboardActivity notificationItems={[]} loading />)
-    expect(TestUtils.select('DashboardActivity__LoadingWrapper')).toBeTruthy()
+  it("renders loading", () => {
+    const { rerender } = render(
+      <DashboardActivity notificationItems={[]} loading />
+    );
+    expect(TestUtils.select("DashboardActivity__LoadingWrapper")).toBeTruthy();
 
-    rerender(<DashboardActivity notificationItems={[]} />)
-    expect(TestUtils.select('DashboardActivity__LoadingWrapper')).toBeFalsy()
-  })
+    rerender(<DashboardActivity notificationItems={[]} />);
+    expect(TestUtils.select("DashboardActivity__LoadingWrapper")).toBeFalsy();
+  });
 
-  it('renders all items', () => {
-    render(<DashboardActivity notificationItems={ITEMS} />)
+  it("renders all items", () => {
+    render(<DashboardActivity notificationItems={ITEMS} />);
 
-    const listItemsEl = TestUtils.selectAll('DashboardActivity__ListItem')
-    expect(listItemsEl.length).toBe(ITEMS.length)
-  })
+    const listItemsEl = TestUtils.selectAll("DashboardActivity__ListItem");
+    expect(listItemsEl.length).toBe(ITEMS.length);
+  });
 
   it.each`
     idx  | href                     | expectedStatusIcon
-    ${0} | ${'/replicas/1'}         | ${'error-hollow.svg'}
-    ${1} | ${'/migrations/2/tasks'} | ${encodedProgressImage}
-    ${2} | ${'/migrations/3'}       | ${'success-hollow.svg'}
-  `('renders item with href $href', ({
-    idx, href, expectedStatusIcon,
-  }) => {
-    render(<DashboardActivity notificationItems={ITEMS} />)
+    ${0} | ${"/replicas/1"}         | ${"error-hollow.svg"}
+    ${1} | ${"/migrations/2/tasks"} | ${encodedProgressImage}
+    ${2} | ${"/migrations/3"}       | ${"success-hollow.svg"}
+  `("renders item with href $href", ({ idx, href, expectedStatusIcon }) => {
+    render(<DashboardActivity notificationItems={ITEMS} />);
 
-    const itemElement = TestUtils.selectAll('DashboardActivity__ListItem')[idx]
-    expect(itemElement.getAttribute('to')).toBe(href)
+    const itemElement = TestUtils.selectAll("DashboardActivity__ListItem")[idx];
+    expect(itemElement.getAttribute("to")).toBe(href);
 
-    const background = window.getComputedStyle(TestUtils.select('StatusIcon__Wrapper', itemElement)!).background
-    expect(background).toContain(expectedStatusIcon)
+    const background = window.getComputedStyle(
+      TestUtils.select("StatusIcon__Wrapper", itemElement)!
+    ).background;
+    expect(background).toContain(expectedStatusIcon);
 
-    expect(TestUtils.select('NotificationDropdown__ItemReplicaBadge', itemElement)!.textContent).toContain(ITEMS[idx].type === 'replica' ? 'RE' : 'MI')
-    expect(TestUtils.select('NotificationDropdown__ItemTitle', itemElement)!.textContent).toContain(ITEMS[idx].name)
-    expect(TestUtils.select('NotificationDropdown__ItemDescription', itemElement)!.textContent).toContain(ITEMS[idx].description)
-  })
-})
+    expect(
+      TestUtils.select("NotificationDropdown__ItemReplicaBadge", itemElement)!
+        .textContent
+    ).toContain(ITEMS[idx].type === "replica" ? "RE" : "MI");
+    expect(
+      TestUtils.select("NotificationDropdown__ItemTitle", itemElement)!
+        .textContent
+    ).toContain(ITEMS[idx].name);
+    expect(
+      TestUtils.select("NotificationDropdown__ItemDescription", itemElement)!
+        .textContent
+    ).toContain(ITEMS[idx].description);
+  });
+});

+ 62 - 49
src/components/modules/DashboardModule/DashboardActivity/DashboardActivity.tsx

@@ -12,38 +12,42 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import * as React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
-import { Link } from 'react-router-dom'
+import * as React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
+import { Link } from "react-router-dom";
 
-import StatusIcon from '@src/components/ui/StatusComponents/StatusIcon'
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
-import Button from '@src/components/ui/Button'
+import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon";
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
+import Button from "@src/components/ui/Button";
 import {
-  InfoColumn, MainItemInfo, ItemReplicaBadge, ItemTitle, ItemDescription,
-} from '@src/components/ui/Dropdowns/NotificationDropdown'
+  InfoColumn,
+  MainItemInfo,
+  ItemReplicaBadge,
+  ItemTitle,
+  ItemDescription,
+} from "@src/components/ui/Dropdowns/NotificationDropdown";
 
-import type { NotificationItemData } from '@src/@types/NotificationItem'
+import type { NotificationItemData } from "@src/@types/NotificationItem";
 
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import replicaImage from './images/replica.svg'
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
+import replicaImage from "./images/replica.svg";
 
 const Wrapper = styled.div<any>`
   flex-grow: 1;
-`
+`;
 const Title = styled.div<any>`
   font-size: 24px;
   font-weight: ${ThemeProps.fontWeights.light};
   margin-bottom: 12px;
-`
+`;
 const Module = styled.div<any>`
   background: ${ThemePalette.grayscale[0]};
   display: flex;
   overflow: hidden;
   border-radius: ${ThemeProps.borderRadius};
   height: 273px;
-`
+`;
 const LoadingWrapper = styled.div<any>`
   width: 100%;
   height: 100%;
@@ -51,13 +55,13 @@ const LoadingWrapper = styled.div<any>`
   justify-content: center;
   align-items: center;
   overflow: hidden;
-`
+`;
 const List = styled.div<any>`
   width: 100%;
   display: flex;
   flex-direction: column;
   flex-wrap: wrap;
-`
+`;
 const ListItem = styled(Link)`
   padding: 8px 16px 8px 16px;
   cursor: pointer;
@@ -69,37 +73,45 @@ const ListItem = styled(Link)`
   &:hover {
     background: ${ThemePalette.grayscale[1]};
   }
-`
+`;
 const NoItems = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
   width: 100%;
-`
+`;
 const ReplicaImage = styled.div<any>`
-  ${ThemeProps.exactSize('148px')}
+  ${ThemeProps.exactSize("148px")}
   background: url('${replicaImage}') center no-repeat;
-`
+`;
 const Message = styled.div<any>`
   text-align: center;
   margin-bottom: 32px;
-`
+`;
 
 type Props = {
-  notificationItems: NotificationItemData[],
-  style?: React.CSSProperties | null,
-  loading?: boolean,
-  large?: boolean,
-  onNewClick?: () => void,
-}
+  notificationItems: NotificationItemData[];
+  style?: React.CSSProperties | null;
+  loading?: boolean;
+  large?: boolean;
+  onNewClick?: () => void;
+};
 @observer
 class DashboardActivity extends React.Component<Props> {
   renderList() {
     return (
       <List>
         {this.props.notificationItems
-          .filter((_, i) => i < (this.props.large ? 10 : 5)).map((item, i) => {
-            const executionsHref = item.status === 'RUNNING' ? item.type === 'replica' ? '/executions' : item.type === 'migration' ? '/tasks' : '' : ''
+          .filter((_, i) => i < (this.props.large ? 10 : 5))
+          .map((item, i) => {
+            const executionsHref =
+              item.status === "RUNNING"
+                ? item.type === "replica"
+                  ? "/executions"
+                  : item.type === "migration"
+                  ? "/tasks"
+                  : ""
+                : "";
 
             return (
               <ListItem
@@ -107,41 +119,40 @@ class DashboardActivity extends React.Component<Props> {
                 to={`/${item.type}s/${item.id}${executionsHref}`}
                 style={{
                   width: `calc(${this.props.large ? 50 : 100}% - 32px)`,
-                  paddingTop: (i === 0 || i === 5) ? '16px' : '8px',
+                  paddingTop: i === 0 || i === 5 ? "16px" : "8px",
                 }}
               >
                 <InfoColumn>
                   <MainItemInfo>
                     <StatusIcon status={item.status} hollow />
-                    <ItemReplicaBadge
-                      type={item.type}
-                    >{item.type === 'replica' ? 'RE' : 'MI'}
+                    <ItemReplicaBadge type={item.type}>
+                      {item.type === "replica" ? "RE" : "MI"}
                     </ItemReplicaBadge>
                     <ItemTitle nowrap>{item.name}</ItemTitle>
                   </MainItemInfo>
                   <ItemDescription>{item.description}</ItemDescription>
                 </InfoColumn>
               </ListItem>
-            )
+            );
           })}
       </List>
-    )
+    );
   }
 
   renderNoItems() {
     return (
       <NoItems>
         <ReplicaImage />
-        <Message>There is no recent activity<br />in this project.</Message>
-        <Button
-          hollow
-          primary
-          transparent
-          onClick={this.props.onNewClick}
-        >New Replica / Migration
+        <Message>
+          There is no recent activity
+          <br />
+          in this project.
+        </Message>
+        <Button hollow primary transparent onClick={this.props.onNewClick}>
+          New Replica / Migration
         </Button>
       </NoItems>
-    )
+    );
   }
 
   renderLoading() {
@@ -149,7 +160,7 @@ class DashboardActivity extends React.Component<Props> {
       <LoadingWrapper>
         <StatusImage status="RUNNING" />
       </LoadingWrapper>
-    )
+    );
   }
 
   render() {
@@ -158,12 +169,14 @@ class DashboardActivity extends React.Component<Props> {
         <Title>Recent Activity</Title>
         <Module>
           {this.props.notificationItems.length === 0 && this.props.loading
-            ? this.renderLoading() : this.props.notificationItems.length
-              ? this.renderList() : this.renderNoItems()}
+            ? this.renderLoading()
+            : this.props.notificationItems.length
+            ? this.renderList()
+            : this.renderNoItems()}
         </Module>
       </Wrapper>
-    )
+    );
   }
 }
 
-export default DashboardActivity
+export default DashboardActivity;

+ 39 - 34
src/components/modules/DashboardModule/DashboardBarChart/BarChartNiceScale.ts

@@ -1,70 +1,75 @@
 class BarChartNiceScale {
-  minPoint: number
+  minPoint: number;
 
-  maxPoint: number
+  maxPoint: number;
 
-  maxTicks: number = 10
+  maxTicks = 10;
 
-  tickSpacing!: number
+  tickSpacing!: number;
 
-  range!: number
+  range!: number;
 
-  niceMinimum!: number
+  niceMinimum!: number;
 
-  niceMaximum!: number
+  niceMaximum!: number;
 
-  constructor(min: number, max: number, maxTicks: number = 10) {
-    this.minPoint = min
-    this.maxPoint = max
-    this.maxTicks = maxTicks
-    this.calculate()
+  constructor(min: number, max: number, maxTicks = 10) {
+    this.minPoint = min;
+    this.maxPoint = max;
+    this.maxTicks = maxTicks;
+    this.calculate();
   }
 
   calculate() {
-    this.range = this.niceNum(this.maxPoint - this.minPoint, false)
-    this.tickSpacing = this.niceNum(this.range / (this.maxTicks - 1), true)
-    this.niceMinimum = Math.floor(this.minPoint / this.tickSpacing) * this.tickSpacing
-    this.niceMaximum = Math.floor(this.maxPoint / this.tickSpacing) * this.tickSpacing
+    this.range = this.niceNum(this.maxPoint - this.minPoint, false);
+    this.tickSpacing = this.niceNum(this.range / (this.maxTicks - 1), true);
+    this.niceMinimum =
+      Math.floor(this.minPoint / this.tickSpacing) * this.tickSpacing;
+    this.niceMaximum =
+      Math.floor(this.maxPoint / this.tickSpacing) * this.tickSpacing;
   }
 
   niceNum(localRange: number, round: boolean) {
-    const exponent = Math.floor(Math.log10(localRange)) /** exponent of localRange */
-    const fraction = localRange / (10 ** exponent) /** fractional part of localRange */
-    let niceFraction /** nice, rounded fraction */
+    const exponent = Math.floor(
+      Math.log10(localRange)
+    ); /** exponent of localRange */
+    const fraction =
+      localRange / 10 ** exponent; /** fractional part of localRange */
+    let niceFraction; /** nice, rounded fraction */
 
     if (round) {
       if (fraction < 1.5) {
-        niceFraction = 1
+        niceFraction = 1;
       } else if (fraction < 3) {
-        niceFraction = 2
+        niceFraction = 2;
       } else if (fraction < 7) {
-        niceFraction = 5
+        niceFraction = 5;
       } else {
-        niceFraction = 10
+        niceFraction = 10;
       }
     } else if (fraction <= 1) {
-      niceFraction = 1
+      niceFraction = 1;
     } else if (fraction <= 2) {
-      niceFraction = 2
+      niceFraction = 2;
     } else if (fraction <= 5) {
-      niceFraction = 5
+      niceFraction = 5;
     } else {
-      niceFraction = 10
+      niceFraction = 10;
     }
 
-    return niceFraction * (10 ** exponent)
+    return niceFraction * 10 ** exponent;
   }
 
   setMinMaxPoints(localMinPoint: number, localMaxPoint: number) {
-    this.minPoint = localMinPoint
-    this.maxPoint = localMaxPoint
-    this.calculate()
+    this.minPoint = localMinPoint;
+    this.maxPoint = localMaxPoint;
+    this.calculate();
   }
 
   setMaxTicks(localMaxTicks: number) {
-    this.maxTicks = localMaxTicks
-    this.calculate()
+    this.maxTicks = localMaxTicks;
+    this.calculate();
   }
 }
 
-export default BarChartNiceScale
+export default BarChartNiceScale;

+ 73 - 46
src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.spec.tsx

@@ -12,45 +12,45 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { render } from '@testing-library/react'
-import TestUtils from '@tests/TestUtils'
-import { ThemePalette } from '@src/components/Theme'
-import userEvent from '@testing-library/user-event'
-import DashboardBarChart from '.'
+import React from "react";
+import { render } from "@testing-library/react";
+import TestUtils from "@tests/TestUtils";
+import { ThemePalette } from "@src/components/Theme";
+import userEvent from "@testing-library/user-event";
+import DashboardBarChart from ".";
 
-const DATA: DashboardBarChart['props']['data'] = [
+const DATA: DashboardBarChart["props"]["data"] = [
   {
-    label: 'label 1',
+    label: "label 1",
     values: [10, 15],
-    data: 'data 1',
+    data: "data 1",
   },
   {
-    label: 'label 2',
+    label: "label 2",
     values: [20, 25],
-    data: 'data 2',
+    data: "data 2",
   },
-]
+];
 
-describe('DashboardBarChart', () => {
-  it('renders all data correctly', () => {
-    render(<DashboardBarChart data={DATA} yNumTicks={3} />)
+describe("DashboardBarChart", () => {
+  it("renders all data correctly", () => {
+    render(<DashboardBarChart data={DATA} yNumTicks={3} />);
 
     // Y ticks
 
-    const yTickEl = TestUtils.selectAll('DashboardBarChart__YTick')
-    expect(yTickEl.length).toBe(3)
-    expect(yTickEl[0].textContent).toBe('0')
-    expect(yTickEl[1].textContent).toBe('20')
-    expect(yTickEl[2].textContent).toBe('40')
+    const yTickEl = TestUtils.selectAll("DashboardBarChart__YTick");
+    expect(yTickEl.length).toBe(3);
+    expect(yTickEl[0].textContent).toBe("0");
+    expect(yTickEl[1].textContent).toBe("20");
+    expect(yTickEl[2].textContent).toBe("40");
 
     // Bars
 
-    const barsEl = TestUtils.selectAll('DashboardBarChart__Bar-')
-    expect(barsEl.length).toBe(DATA.length)
-    expect(barsEl[0].textContent).toBe('label 1')
-    expect(barsEl[1].textContent).toBe('label 2')
-  })
+    const barsEl = TestUtils.selectAll("DashboardBarChart__Bar-");
+    expect(barsEl.length).toBe(DATA.length);
+    expect(barsEl[0].textContent).toBe("label 1");
+    expect(barsEl[1].textContent).toBe("label 2");
+  });
 
   it.each`
     barIndex | stackedBarIndex | expectedHeight                    | expectedColor
@@ -58,28 +58,55 @@ describe('DashboardBarChart', () => {
     ${0}     | ${1}            | ${(DATA[0].values[0] / 45) * 100} | ${ThemePalette.primary}
     ${1}     | ${0}            | ${(DATA[1].values[1] / 45) * 100} | ${ThemePalette.alert}
     ${1}     | ${1}            | ${(DATA[1].values[0] / 45) * 100} | ${ThemePalette.primary}
-  `('renders bar index $barIndex, stacked bar index $stackedBarIndex with height $expectedHeight and color $expectedColor', ({
-    barIndex, stackedBarIndex, expectedHeight, expectedColor,
-  }) => {
-    render(<DashboardBarChart data={DATA} yNumTicks={3} colors={[ThemePalette.alert, ThemePalette.primary]} />)
+  `(
+    "renders bar index $barIndex, stacked bar index $stackedBarIndex with height $expectedHeight and color $expectedColor",
+    ({ barIndex, stackedBarIndex, expectedHeight, expectedColor }) => {
+      render(
+        <DashboardBarChart
+          data={DATA}
+          yNumTicks={3}
+          colors={[ThemePalette.alert, ThemePalette.primary]}
+        />
+      );
 
-    const stackedBarEl = TestUtils.selectAll('DashboardBarChart__StackedBar-', TestUtils.selectAll('DashboardBarChart__Bar-')[barIndex])[stackedBarIndex]
-    const style = window.getComputedStyle(stackedBarEl)
+      const stackedBarEl = TestUtils.selectAll(
+        "DashboardBarChart__StackedBar-",
+        TestUtils.selectAll("DashboardBarChart__Bar-")[barIndex]
+      )[stackedBarIndex];
+      const style = window.getComputedStyle(stackedBarEl);
 
-    expect(parseFloat(style.height)).toBeCloseTo(expectedHeight)
-    expect(TestUtils.rgbToHex(style.background)).toBe(expectedColor)
-  })
+      expect(parseFloat(style.height)).toBeCloseTo(expectedHeight);
+      expect(TestUtils.rgbToHex(style.background)).toBe(expectedColor);
+    }
+  );
 
   it.each`
-  barIndex | stackedBarIndex | expectedData
-  ${0}     | ${0}            | ${DATA[0]}
-  ${0}     | ${1}            | ${DATA[0]}
-  ${1}     | ${0}            | ${DATA[1]}
-  ${1}     | ${1}            | ${DATA[1]}
-`('fires mouse position with correct data on bar mouse enter, bar index $barIndex, stacked bar index $stackedBarIndex', ({ barIndex, stackedBarIndex, expectedData }) => {
-    const onBarMouseEnter = jest.fn()
-    render(<DashboardBarChart data={DATA} yNumTicks={3} onBarMouseEnter={onBarMouseEnter} />)
-    userEvent.hover(TestUtils.selectAll('DashboardBarChart__StackedBar-', TestUtils.selectAll('DashboardBarChart__Bar-')[barIndex])[stackedBarIndex])
-    expect(onBarMouseEnter).toHaveBeenCalledWith({ x: 48, y: 65 }, expectedData)
-  })
-})
+    barIndex | stackedBarIndex | expectedData
+    ${0}     | ${0}            | ${DATA[0]}
+    ${0}     | ${1}            | ${DATA[0]}
+    ${1}     | ${0}            | ${DATA[1]}
+    ${1}     | ${1}            | ${DATA[1]}
+  `(
+    "fires mouse position with correct data on bar mouse enter, bar index $barIndex, stacked bar index $stackedBarIndex",
+    ({ barIndex, stackedBarIndex, expectedData }) => {
+      const onBarMouseEnter = jest.fn();
+      render(
+        <DashboardBarChart
+          data={DATA}
+          yNumTicks={3}
+          onBarMouseEnter={onBarMouseEnter}
+        />
+      );
+      userEvent.hover(
+        TestUtils.selectAll(
+          "DashboardBarChart__StackedBar-",
+          TestUtils.selectAll("DashboardBarChart__Bar-")[barIndex]
+        )[stackedBarIndex]
+      );
+      expect(onBarMouseEnter).toHaveBeenCalledWith(
+        { x: 48, y: 65 },
+        expectedData
+      );
+    }
+  );
+});

+ 93 - 69
src/components/modules/DashboardModule/DashboardBarChart/DashboardBarChart.tsx

@@ -12,23 +12,23 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import * as React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
+import * as React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
 
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import BarChartNiceScale from './BarChartNiceScale'
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
+import BarChartNiceScale from "./BarChartNiceScale";
 
 const Wrapper = styled.div<any>`
   position: relative;
   width: 100%;
-`
+`;
 const YAxis = styled.div<any>`
   height: calc(100% - 24px);
   position: absolute;
   bottom: 24px;
   left: 16px;
-`
+`;
 const YTick = styled.div<any>`
   position: absolute;
   top: ${props => 100 - props.bottom}%;
@@ -38,21 +38,21 @@ const YTick = styled.div<any>`
   overflow: hidden;
   text-overflow: ellipsis;
   text-align: right;
-`
+`;
 const GridLines = styled.div<any>`
   width: calc(100% - 64px);
   height: calc(100% - 24px);
   position: absolute;
   bottom: 19px;
   left: 48px;
-`
+`;
 const GridLine = styled.div<any>`
   position: absolute;
   bottom: ${props => props.bottom}%;
   height: 1px;
   width: 100%;
   background: white;
-`
+`;
 const Bars = styled.div<any>`
   position: absolute;
   display: flex;
@@ -61,18 +61,18 @@ const Bars = styled.div<any>`
   justify-content: space-around;
   left: 48px;
   bottom: 2px;
-`
+`;
 const Bar = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
-`
+`;
 const StackedBars = styled.div<any>`
   display: flex;
   flex-direction: column;
   height: 100%;
   justify-content: flex-end;
-`
+`;
 const StackedBar = styled.div<any>`
   width: 16px;
   height: ${props => props.height}%;
@@ -81,136 +81,160 @@ const StackedBar = styled.div<any>`
     border-top-left-radius: 3px;
     border-top-right-radius: 3px;
   }
-`
+`;
 const BarLabel = styled.div<any>`
   font-size: 9px;
   font-weight: ${ThemeProps.fontWeights.medium};
   margin-top: 8px;
-`
+`;
 type DataItem = {
-  label: string,
-  values: number[],
-  data?: any,
-}
+  label: string;
+  values: number[];
+  data?: any;
+};
 type Props = {
-  style?: any,
-  data: DataItem[],
-  yNumTicks: number,
-  colors?: string[],
-  onBarMouseEnter?: (position: { x: number, y: number }, item: DataItem) => void,
-  onBarMouseLeave?: () => void,
-}
+  style?: any;
+  data: DataItem[];
+  yNumTicks: number;
+  colors?: string[];
+  onBarMouseEnter?: (
+    position: { x: number; y: number },
+    item: DataItem
+  ) => void;
+  onBarMouseLeave?: () => void;
+};
 
 @observer
 class DashboardBarChart extends React.Component<Props> {
-  barsRef: HTMLElement | null | undefined
+  barsRef: HTMLElement | null | undefined;
 
-  ticks: { value: number }[] = []
+  ticks: { value: number }[] = [];
 
-  range: number = 1
+  range = 1;
 
   UNSAFE_componentWillMount() {
-    this.calculateYTicks(this.props)
+    this.calculateYTicks(this.props);
   }
 
   UNSAFE_componentWillReceiveProps(props: Props) {
-    this.calculateYTicks(props)
+    this.calculateYTicks(props);
   }
 
   calculateYTicks(props: Props) {
-    this.range = props.data.reduce((max, item) => Math.max(max, item.values.reduce((sum, value) => sum + value, 0)), 1)
-    const niceScale = new BarChartNiceScale(0, this.range, props.yNumTicks)
-    this.ticks = []
-    const numTicks = Math.floor(this.range / niceScale.tickSpacing) + 1
+    this.range = props.data.reduce(
+      (max, item) =>
+        Math.max(
+          max,
+          item.values.reduce((sum, value) => sum + value, 0)
+        ),
+      1
+    );
+    const niceScale = new BarChartNiceScale(0, this.range, props.yNumTicks);
+    this.ticks = [];
+    const numTicks = Math.floor(this.range / niceScale.tickSpacing) + 1;
     for (let i = 0; i < numTicks; i += 1) {
-      this.ticks.push({ value: i * niceScale.tickSpacing })
+      this.ticks.push({ value: i * niceScale.tickSpacing });
     }
   }
 
-  calculatePosition(evt: MouseEvent): { x: number, y: number } {
-    const targetMouse: any = evt.currentTarget
-    const target: HTMLElement = targetMouse.parentElement
-    let height = 0
+  calculatePosition(evt: MouseEvent): { x: number; y: number } {
+    const targetMouse: any = evt.currentTarget;
+    const target: HTMLElement = targetMouse.parentElement;
+    let height = 0;
     target.childNodes.forEach(node => {
-      const element: any = node
-      height += element.offsetHeight
-    })
+      const element: any = node;
+      height += element.offsetHeight;
+    });
     return {
       x: target.offsetLeft + 48,
       y: height + 65,
-    }
+    };
   }
 
   renderYAxis() {
     return (
       <YAxis>
         {this.ticks.map(tick => (
-          <YTick key={tick.value} bottom={(tick.value / this.range) * 100}>{tick.value}</YTick>
+          <YTick key={tick.value} bottom={(tick.value / this.range) * 100}>
+            {tick.value}
+          </YTick>
         ))}
       </YAxis>
-    )
+    );
   }
 
   renderGridLines() {
-    const gridLines: { value: number }[] = []
+    const gridLines: { value: number }[] = [];
     this.ticks.forEach((tick, i) => {
-      gridLines.push({ value: tick.value })
+      gridLines.push({ value: tick.value });
       if (i === this.ticks.length - 1) {
-        return
+        return;
       }
-      gridLines.push({ value: (this.ticks[i + 1].value + tick.value) / 2 })
-    })
+      gridLines.push({ value: (this.ticks[i + 1].value + tick.value) / 2 });
+    });
     return (
       <GridLines>
         {gridLines.map(gridline => (
-          <GridLine key={gridline.value} bottom={(gridline.value / this.range) * 100} />
+          <GridLine
+            key={gridline.value}
+            bottom={(gridline.value / this.range) * 100}
+          />
         ))}
       </GridLines>
-    )
+    );
   }
 
   renderBars() {
-    let availableWidth = window.innerWidth
+    let availableWidth = window.innerWidth;
     if (this.barsRef) {
-      availableWidth = this.barsRef.offsetWidth
+      availableWidth = this.barsRef.offsetWidth;
     }
-    let items = this.props.data
-    if ((30 * items.length) > availableWidth) {
-      items = items.filter((_, i) => i % 2)
+    let items = this.props.data;
+    if (30 * items.length > availableWidth) {
+      items = items.filter((_, i) => i % 2);
     }
 
     return (
-      <Bars ref={(ref: HTMLElement | null | undefined) => { this.barsRef = ref }}>
+      <Bars
+        ref={(ref: HTMLElement | null | undefined) => {
+          this.barsRef = ref;
+        }}
+      >
         {items.map(item => (
           <Bar key={item.label}>
             <StackedBars>
               {[...item.values].reverse().map((value, i) => {
-                const height = (value / this.range) * 100
+                const height = (value / this.range) * 100;
                 return height > 0 ? (
                   <StackedBar
                     // eslint-disable-next-line react/no-array-index-key
                     key={`${item.label}-${i}`}
-                    background={this.props.colors ? this.props.colors[i % this.props.colors.length] : ThemePalette.primary}
+                    background={
+                      this.props.colors
+                        ? this.props.colors[i % this.props.colors.length]
+                        : ThemePalette.primary
+                    }
                     height={height}
                     onMouseEnter={(evt: MouseEvent) => {
-                      const onMouseEnter = this.props.onBarMouseEnter
+                      const onMouseEnter = this.props.onBarMouseEnter;
                       if (!onMouseEnter) {
-                        return
+                        return;
                       }
-                      onMouseEnter(this.calculatePosition(evt), item)
+                      onMouseEnter(this.calculatePosition(evt), item);
                     }}
                     onMouseLeave={() => {
-                      if (this.props.onBarMouseLeave) this.props.onBarMouseLeave()
+                      if (this.props.onBarMouseLeave)
+                        this.props.onBarMouseLeave();
                     }}
                   />
-                ) : null
+                ) : null;
               })}
             </StackedBars>
             <BarLabel>{item.label}</BarLabel>
           </Bar>
         ))}
       </Bars>
-    )
+    );
   }
 
   render() {
@@ -220,8 +244,8 @@ class DashboardBarChart extends React.Component<Props> {
         {this.renderGridLines()}
         {this.renderBars()}
       </Wrapper>
-    )
+    );
   }
 }
 
-export default DashboardBarChart
+export default DashboardBarChart;

+ 86 - 78
src/components/modules/DashboardModule/DashboardContent/DashboardContent.tsx

@@ -12,31 +12,31 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
-import autobind from 'autobind-decorator'
+import React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
+import autobind from "autobind-decorator";
 
-import DashboardInfoCount from '@src/components/modules/DashboardModule/DashboardInfoCount'
-import DashboardLicence from '@src/components/modules/DashboardModule/DashboardLicence'
-import DashboardActivity from '@src/components/modules/DashboardModule/DashboardActivity'
-import DashboardTopEndpoints from '@src/components/modules/DashboardModule/DashboardTopEndpoints'
-import DashboardExecutions from '@src/components/modules/DashboardModule/DashboardExecutions'
+import DashboardInfoCount from "@src/components/modules/DashboardModule/DashboardInfoCount";
+import DashboardLicence from "@src/components/modules/DashboardModule/DashboardLicence";
+import DashboardActivity from "@src/components/modules/DashboardModule/DashboardActivity";
+import DashboardTopEndpoints from "@src/components/modules/DashboardModule/DashboardTopEndpoints";
+import DashboardExecutions from "@src/components/modules/DashboardModule/DashboardExecutions";
 
-import { ThemePalette } from '@src/components/Theme'
+import { ThemePalette } from "@src/components/Theme";
 
-import type { Endpoint } from '@src/@types/Endpoint'
-import type { Project } from '@src/@types/Project'
-import type { User } from '@src/@types/User'
-import type { Licence, LicenceServerStatus } from '@src/@types/Licence'
-import type { NotificationItemData } from '@src/@types/NotificationItem'
-import { ReplicaItem, MigrationItem } from '@src/@types/MainItem'
+import type { Endpoint } from "@src/@types/Endpoint";
+import type { Project } from "@src/@types/Project";
+import type { User } from "@src/@types/User";
+import type { Licence, LicenceServerStatus } from "@src/@types/Licence";
+import type { NotificationItemData } from "@src/@types/NotificationItem";
+import { ReplicaItem, MigrationItem } from "@src/@types/MainItem";
 
-const MIDDLE_WIDTHS = ['264px', '264px', '264px']
+const MIDDLE_WIDTHS = ["264px", "264px", "264px"];
 
 const Wrapper = styled.div<any>`
   margin-bottom: 64px;
-`
+`;
 const RowLayout = styled.div<any>`
   display: flex;
   flex-wrap: wrap;
@@ -46,85 +46,94 @@ const RowLayout = styled.div<any>`
     margin-top: 40px;
     margin-left: 32px;
   }
-`
+`;
 const MiddleMobileLayout = styled.div<any>`
   margin: 40px 0;
-`
+`;
 
 type Props = {
-  replicas: ReplicaItem[],
-  migrations: MigrationItem[],
-  endpoints: Endpoint[],
-  projects: Project[],
-  replicasLoading: boolean,
-  migrationsLoading: boolean,
-  endpointsLoading: boolean,
-  usersLoading: boolean,
-  projectsLoading: boolean,
-  licenceLoading: boolean,
-  notificationItemsLoading: boolean,
-  users: User[],
-  licence: Licence | null,
-  licenceServerStatus: LicenceServerStatus | null
-  licenceError: string | null,
-  notificationItems: NotificationItemData[],
-  isAdmin: boolean,
-  onNewReplicaClick: () => void,
-  onNewEndpointClick: () => void,
-  onAddLicenceClick: () => void,
-}
+  replicas: ReplicaItem[];
+  migrations: MigrationItem[];
+  endpoints: Endpoint[];
+  projects: Project[];
+  replicasLoading: boolean;
+  migrationsLoading: boolean;
+  endpointsLoading: boolean;
+  usersLoading: boolean;
+  projectsLoading: boolean;
+  licenceLoading: boolean;
+  notificationItemsLoading: boolean;
+  users: User[];
+  licence: Licence | null;
+  licenceServerStatus: LicenceServerStatus | null;
+  licenceError: string | null;
+  notificationItems: NotificationItemData[];
+  isAdmin: boolean;
+  onNewReplicaClick: () => void;
+  onNewEndpointClick: () => void;
+  onAddLicenceClick: () => void;
+};
 type State = {
-  useMobileLayout: boolean,
-  useLargeActivity: boolean,
-}
+  useMobileLayout: boolean;
+  useLargeActivity: boolean;
+};
 @observer
 class DashboardContent extends React.Component<Props, State> {
   state = {
     useMobileLayout: false,
     useLargeActivity: false,
-  }
+  };
 
   UNSAFE_componentWillMount() {
-    this.handleResize()
-    window.addEventListener('resize', this.handleResize)
+    this.handleResize();
+    window.addEventListener("resize", this.handleResize);
   }
 
   componentWillUnmount() {
-    window.removeEventListener('resize', this.handleResize, false)
+    window.removeEventListener("resize", this.handleResize, false);
   }
 
   @autobind
   handleResize() {
     if (window.innerWidth < 1120 && !this.state.useMobileLayout) {
-      this.setState({ useMobileLayout: true })
+      this.setState({ useMobileLayout: true });
     } else if (window.innerWidth >= 1120 && this.state.useMobileLayout) {
-      this.setState({ useMobileLayout: false })
+      this.setState({ useMobileLayout: false });
     }
     if (window.innerWidth >= 2100 && !this.state.useLargeActivity) {
-      this.setState({ useLargeActivity: true })
+      this.setState({ useLargeActivity: true });
     } else if (window.innerWidth < 2100 && this.state.useLargeActivity) {
-      this.setState({ useLargeActivity: false })
+      this.setState({ useLargeActivity: false });
     }
   }
 
   renderMiddleModules() {
     const modules = [
       <DashboardActivity
+        key="activity"
         large={this.state.useMobileLayout || this.state.useLargeActivity}
         notificationItems={this.props.notificationItems}
         loading={this.props.notificationItemsLoading}
-        style={this.state.useMobileLayout ? null : {
-          minWidth: MIDDLE_WIDTHS[0],
-          width: MIDDLE_WIDTHS[0],
-        }}
+        style={
+          this.state.useMobileLayout
+            ? null
+            : {
+                minWidth: MIDDLE_WIDTHS[0],
+                width: MIDDLE_WIDTHS[0],
+              }
+        }
         onNewClick={this.props.onNewReplicaClick}
       />,
       <DashboardTopEndpoints
+        key="top-endpoints"
         replicas={this.props.replicas}
         migrations={this.props.migrations}
         endpoints={this.props.endpoints}
-        loading={this.props.replicasLoading
-          || this.props.migrationsLoading || this.props.endpointsLoading}
+        loading={
+          this.props.replicasLoading ||
+          this.props.migrationsLoading ||
+          this.props.endpointsLoading
+        }
         style={{
           minWidth: MIDDLE_WIDTHS[1],
           width: MIDDLE_WIDTHS[1],
@@ -132,6 +141,7 @@ class DashboardContent extends React.Component<Props, State> {
         onNewClick={this.props.onNewEndpointClick}
       />,
       <DashboardLicence
+        key="licence"
         licence={this.props.licence}
         loading={this.props.licenceLoading}
         licenceServerStatus={this.props.licenceServerStatus}
@@ -142,7 +152,7 @@ class DashboardContent extends React.Component<Props, State> {
           width: MIDDLE_WIDTHS[2],
         }}
       />,
-    ]
+    ];
 
     if (this.state.useMobileLayout) {
       return (
@@ -153,7 +163,7 @@ class DashboardContent extends React.Component<Props, State> {
             {modules[2]}
           </RowLayout>
         </MiddleMobileLayout>
-      )
+      );
     }
 
     return (
@@ -162,58 +172,56 @@ class DashboardContent extends React.Component<Props, State> {
         {modules[1]}
         {modules[2]}
       </RowLayout>
-    )
+    );
   }
 
   render() {
     let infoCountData = [
       {
-        label: 'Replicas',
+        label: "Replicas",
         value: this.props.replicas.length,
         color: ThemePalette.alert,
-        link: '/replicas',
+        link: "/replicas",
         loading: this.props.replicasLoading,
       },
       {
-        label: 'Migrations',
+        label: "Migrations",
         value: this.props.migrations.length,
         color: ThemePalette.primary,
-        link: '/migrations',
+        link: "/migrations",
         loading: this.props.migrationsLoading,
       },
       {
-        label: 'Endpoints',
+        label: "Endpoints",
         value: this.props.endpoints.length,
         color: ThemePalette.black,
-        link: '/endpoints',
+        link: "/endpoints",
         loading: this.props.endpointsLoading,
       },
-    ]
+    ];
 
     if (this.props.isAdmin) {
       infoCountData = infoCountData.concat([
         {
-          label: 'Users',
+          label: "Users",
           value: this.props.users.length,
           color: ThemePalette.grayscale[3],
-          link: '/users',
+          link: "/users",
           loading: this.props.usersLoading,
         },
         {
-          label: 'Projects',
+          label: "Projects",
           value: this.props.projects.length,
           color: ThemePalette.grayscale[3],
-          link: '/projects',
+          link: "/projects",
           loading: this.props.projectsLoading,
         },
-      ])
+      ]);
     }
 
     return (
       <Wrapper>
-        <DashboardInfoCount
-          data={infoCountData}
-        />
+        <DashboardInfoCount data={infoCountData} />
         {this.renderMiddleModules()}
         <DashboardExecutions
           replicas={this.props.replicas}
@@ -221,8 +229,8 @@ class DashboardContent extends React.Component<Props, State> {
           loading={this.props.replicasLoading || this.props.migrationsLoading}
         />
       </Wrapper>
-    )
+    );
   }
 }
 
-export default DashboardContent
+export default DashboardContent;

+ 0 - 1
src/components/modules/DashboardModule/DashboardContent/package.json

@@ -4,4 +4,3 @@
   "private": true,
   "main": "./DashboardContent.tsx"
 }
-

+ 128 - 104
src/components/modules/DashboardModule/DashboardExecutions/DashboardExecutions.tsx

@@ -12,49 +12,49 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import * as React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
-import moment from 'moment'
+import * as React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
+import moment from "moment";
 
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
-import DropdownLink from '@src/components/ui/Dropdowns/DropdownLink'
-import DashboardBarChart from '@src/components/modules/DashboardModule/DashboardBarChart'
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
+import DropdownLink from "@src/components/ui/Dropdowns/DropdownLink";
+import DashboardBarChart from "@src/components/modules/DashboardModule/DashboardBarChart";
 
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
 
-import { ReplicaItem, MigrationItem, TransferItem } from '@src/@types/MainItem'
-import emptyBackgroundImage from './images/empty-background.svg'
+import { ReplicaItem, MigrationItem, TransferItem } from "@src/@types/MainItem";
+import emptyBackgroundImage from "./images/empty-background.svg";
 
 const INTERVALS = [
-  { label: 'Last {x} days', value: '30-days' },
-  { label: 'Last 12 months', value: '1-years' },
-]
+  { label: "Last {x} days", value: "30-days" },
+  { label: "Last 12 months", value: "1-years" },
+];
 
-const Wrapper = styled.div<any>``
+const Wrapper = styled.div<any>``;
 const Title = styled.div<any>`
   font-size: 24px;
   font-weight: ${ThemeProps.fontWeights.light};
   margin-bottom: 12px;
-`
+`;
 const Module = styled.div<any>`
   position: relative;
   display: flex;
   background: ${ThemePalette.grayscale[0]};
   border-radius: ${ThemeProps.borderRadius};
   height: 240px;
-`
+`;
 const ChartWrapper = styled.div<any>`
   display: flex;
   flex-direction: column;
   height: 100%;
   width: 100%;
-`
+`;
 const BarChartWrapper = styled.div<any>`
   display: flex;
   height: 100%;
   width: 100%;
-`
+`;
 const LoadingWrapper = styled.div<any>`
   display: flex;
   width: 100%;
@@ -62,12 +62,12 @@ const LoadingWrapper = styled.div<any>`
   overflow: hidden;
   justify-content: center;
   align-items: center;
-`
+`;
 const DropdownWrapper = styled.div<any>`
   display: flex;
   justify-content: flex-end;
   margin: 16px;
-`
+`;
 const Tooltip = styled.div<any>`
   position: absolute;
   bottom: ${props => props.position.y}px;
@@ -76,25 +76,25 @@ const Tooltip = styled.div<any>`
   padding: 8px 16px 16px 16px;
   border-radius: ${ThemeProps.borderRadius};
   color: white;
-  ${ThemeProps.exactWidth('174px')}
+  ${ThemeProps.exactWidth("174px")}
   box-shadow: rgba(0,0,0,0.1) 0 0 6px 1px;
-`
+`;
 const TooltipHeader = styled.div<any>`
   font-size: 24px;
   font-weight: ${ThemeProps.fontWeights.light};
   text-align: center;
   border-bottom: 1px solid;
   padding-bottom: 4px;
-`
+`;
 const TooltipBody = styled.div<any>`
   font-size: 12px;
-`
+`;
 const TooltipRow = styled.div<any>`
   display: flex;
   justify-content: space-between;
   margin-top: 16px;
-`
-const TooltipRowLabel = styled.div<any>``
+`;
+const TooltipRowLabel = styled.div<any>``;
 const TooltipTip = styled.div<any>`
   position: absolute;
   width: 16px;
@@ -103,11 +103,11 @@ const TooltipTip = styled.div<any>`
   background: ${ThemePalette.black};
   left: calc(50% - 16px);
   transform: rotate(45deg);
-`
+`;
 const NoData = styled.div<any>`
   padding: 0 16px;
   position: relative;
-`
+`;
 const NoDataMessage = styled.div<any>`
   position: absolute;
   font-size: 17px;
@@ -119,37 +119,37 @@ const NoDataMessage = styled.div<any>`
   left: 0;
   justify-content: center;
   align-items: center;
-  text-shadow: rgba(255,255,255,1) 0px 0px 20px;
-`
+  text-shadow: rgba(255, 255, 255, 1) 0px 0px 20px;
+`;
 const EmptyBackgroundImage = styled.div<any>`
   width: 100%;
   height: 146px;
-  background: url('${emptyBackgroundImage}');
-`
+  background: url("${emptyBackgroundImage}");
+`;
 
 type Props = {
   // eslint-disable-next-line react/no-unused-prop-types
-  replicas: ReplicaItem[],
-  migrations: MigrationItem[],
-  loading: boolean,
-}
+  replicas: ReplicaItem[];
+  migrations: MigrationItem[];
+  loading: boolean;
+};
 type GroupedData = {
-  label: string,
-  values: number[],
-  data?: string,
-}
+  label: string;
+  values: number[];
+  data?: string;
+};
 type TooltipData = {
-  title: string,
-  migrations: number,
-  replicas: number,
-}
+  title: string;
+  migrations: number;
+  replicas: number;
+};
 type State = {
-  selectedPeriod: string,
-  groupedData: GroupedData[],
-  tooltipPosition: { x: number, y: number },
-  tooltipData: TooltipData | null,
-}
-const COLORS = [ThemePalette.alert, ThemePalette.primary]
+  selectedPeriod: string;
+  groupedData: GroupedData[];
+  tooltipPosition: { x: number; y: number };
+  tooltipData: TooltipData | null;
+};
+const COLORS = [ThemePalette.alert, ThemePalette.primary];
 
 @observer
 class DashboardExecutions extends React.Component<Props, State> {
@@ -158,102 +158,119 @@ class DashboardExecutions extends React.Component<Props, State> {
     groupedData: [],
     tooltipData: null,
     tooltipPosition: { x: 0, y: 0 },
-  }
+  };
 
   componentDidMount() {
-    this.groupCreations(this.props)
+    this.groupCreations(this.props);
   }
 
   UNSAFE_componentWillReceiveProps(props: Props) {
-    this.groupCreations(props)
+    this.groupCreations(props);
   }
 
   groupCreations(props: Props) {
-    let creations: TransferItem[] = [...props.replicas, ...props.migrations]
+    let creations: TransferItem[] = [...props.replicas, ...props.migrations];
 
-    const periodUnit: any = this.state.selectedPeriod.split('-')[1]
-    const periodValue: any = Number(this.state.selectedPeriod.split('-')[0])
-    const oldestDate: Date = moment().subtract(periodValue, periodUnit).toDate()
-    creations = creations
-      .filter(e => new Date(e.created_at).getTime() >= oldestDate.getTime())
-    creations.sort((a, b) => new Date(a.created_at).getTime()
-      - new Date(b.created_at).getTime())
+    const periodUnit: any = this.state.selectedPeriod.split("-")[1];
+    const periodValue: any = Number(this.state.selectedPeriod.split("-")[0]);
+    const oldestDate: Date = moment()
+      .subtract(periodValue, periodUnit)
+      .toDate();
+    creations = creations.filter(
+      e => new Date(e.created_at).getTime() >= oldestDate.getTime()
+    );
+    creations.sort(
+      (a, b) =>
+        new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
+    );
 
-    this.groupByPeriod(creations, periodUnit)
+    this.groupByPeriod(creations, periodUnit);
   }
 
   groupByPeriod(transferItems: TransferItem[], periodUnit: string) {
-    const groupedData: GroupedData[] = []
-    const periods: { [period: string]: { replicas: number, migrations: number } } = {}
+    const groupedData: GroupedData[] = [];
+    const periods: {
+      [period: string]: { replicas: number; migrations: number };
+    } = {};
     transferItems.forEach(item => {
-      const date = moment(new Date(item.created_at))
-      const period: string = periodUnit === 'days' ? date.format('DD-MMM-YYYY_DD MMMM') : date.format('MMM-YYYY_MMMM YYYY')
+      const date = moment(new Date(item.created_at));
+      const period: string =
+        periodUnit === "days"
+          ? date.format("DD-MMM-YYYY_DD MMMM")
+          : date.format("MMM-YYYY_MMMM YYYY");
       if (!periods[period]) {
-        periods[period] = { replicas: 0, migrations: 0 }
+        periods[period] = { replicas: 0, migrations: 0 };
       }
-      if (item.type === 'replica') {
-        periods[period].replicas += 1
-      } else if (item.type === 'migration') {
-        periods[period].migrations += 1
+      if (item.type === "replica") {
+        periods[period].replicas += 1;
+      } else if (item.type === "migration") {
+        periods[period].migrations += 1;
       }
-    })
+    });
     Object.keys(periods).forEach(period => {
       if (!periods[period].replicas && !periods[period].migrations) {
-        return
+        return;
       }
-      const label = period.split('_')[0]
-      const title = period.split('_')[1]
+      const label = period.split("_")[0];
+      const title = period.split("_")[1];
       groupedData.push({
-        label: periodUnit === 'days' ? `${label.split('-')[0]} ${label.split('-')[1]}` : label.split('-')[0],
+        label:
+          periodUnit === "days"
+            ? `${label.split("-")[0]} ${label.split("-")[1]}`
+            : label.split("-")[0],
         values: [periods[period].migrations, periods[period].replicas],
         data: title,
-      })
-    })
-    this.setState({ groupedData })
+      });
+    });
+    this.setState({ groupedData });
   }
 
   handleDropdownChange(selectedPeriod: string) {
     this.setState({ selectedPeriod }, () => {
-      this.groupCreations(this.props)
-    })
+      this.groupCreations(this.props);
+    });
   }
 
-  handleBarMouseEnter(position: { x: number, y: number }, item: GroupedData) {
+  handleBarMouseEnter(position: { x: number; y: number }, item: GroupedData) {
     this.setState({
       tooltipPosition: { x: position.x - 86, y: position.y },
       tooltipData: {
         replicas: item.values[1],
         migrations: item.values[0],
-        title: item.data || '-',
+        title: item.data || "-",
       },
-    })
+    });
   }
 
   handleBarMouseLeave() {
-    this.setState({ tooltipData: null })
+    this.setState({ tooltipData: null });
   }
 
   renderDropdown() {
     const items = INTERVALS.map(interval => ({
       value: interval.value,
-      label: interval.label.replace('{x}', interval.value.split('-')[0]),
-    }))
-    const selectedItem = INTERVALS.find(i => i.value === this.state.selectedPeriod)
+      label: interval.label.replace("{x}", interval.value.split("-")[0]),
+    }));
+    const selectedItem = INTERVALS.find(
+      i => i.value === this.state.selectedPeriod
+    );
     return (
       <DropdownWrapper>
         <DropdownLink
           items={items}
           selectedItem={selectedItem && selectedItem.value}
-          onChange={item => { this.handleDropdownChange(item.value) }}
+          onChange={item => {
+            this.handleDropdownChange(item.value);
+          }}
         />
       </DropdownWrapper>
-    )
+    );
   }
 
   renderTooltip() {
-    const data = this.state.tooltipData
+    const data = this.state.tooltipData;
     if (!data) {
-      return null
+      return null;
     }
     return (
       <Tooltip position={this.state.tooltipPosition}>
@@ -274,32 +291,38 @@ class DashboardExecutions extends React.Component<Props, State> {
         </TooltipBody>
         <TooltipTip />
       </Tooltip>
-    )
+    );
   }
 
   renderBarChart() {
     return (
       <BarChartWrapper>
         <DashboardBarChart
-          style={{ height: '164px' }}
+          style={{ height: "164px" }}
           yNumTicks={3}
           data={this.state.groupedData}
           colors={COLORS}
-          onBarMouseEnter={(position, item) => { this.handleBarMouseEnter(position, item) }}
-          onBarMouseLeave={() => { this.handleBarMouseLeave() }}
+          onBarMouseEnter={(position, item) => {
+            this.handleBarMouseEnter(position, item);
+          }}
+          onBarMouseLeave={() => {
+            this.handleBarMouseLeave();
+          }}
         />
         {this.renderTooltip()}
       </BarChartWrapper>
-    )
+    );
   }
 
   renderChart() {
     return (
       <ChartWrapper>
         {this.renderDropdown()}
-        {this.state.groupedData.length ? this.renderBarChart() : this.renderNoData()}
+        {this.state.groupedData.length
+          ? this.renderBarChart()
+          : this.renderNoData()}
       </ChartWrapper>
-    )
+    );
   }
 
   renderLoading() {
@@ -307,7 +330,7 @@ class DashboardExecutions extends React.Component<Props, State> {
       <LoadingWrapper>
         <StatusImage status="RUNNING" />
       </LoadingWrapper>
-    )
+    );
   }
 
   renderNoData() {
@@ -316,7 +339,7 @@ class DashboardExecutions extends React.Component<Props, State> {
         <EmptyBackgroundImage />
         <NoDataMessage>No recent activity in this project</NoDataMessage>
       </NoData>
-    )
+    );
   }
 
   render() {
@@ -325,11 +348,12 @@ class DashboardExecutions extends React.Component<Props, State> {
         <Title>Items Created</Title>
         <Module>
           {this.props.replicas.length === 0 && this.props.loading
-            ? this.renderLoading() : this.renderChart()}
+            ? this.renderLoading()
+            : this.renderChart()}
         </Module>
       </Wrapper>
-    )
+    );
   }
 }
 
-export default DashboardExecutions
+export default DashboardExecutions;

+ 30 - 25
src/components/modules/DashboardModule/DashboardInfoCount/DashboardInfoCount.tsx

@@ -12,21 +12,21 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
-import { Link } from 'react-router-dom'
+import React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
+import { Link } from "react-router-dom";
 
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
 
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
 
 const Wrapper = styled.div<any>`
   background: ${ThemePalette.grayscale[0]};
   display: flex;
   overflow: auto;
   border-radius: ${ThemeProps.borderRadius};
-`
+`;
 const CountBlock = styled.div<any>`
   flex-grow: 1;
   display: flex;
@@ -44,33 +44,33 @@ const CountBlock = styled.div<any>`
   @media (max-width: 832px) {
     align-items: flex-start;
   }
-`
+`;
 const LoadingWrapper = styled.div<any>`
   overflow: hidden;
   margin-bottom: 16px;
-`
+`;
 const CountBlockValue = styled(Link)`
   font-size: 53px;
   font-weight: ${ThemeProps.fontWeights.extraLight};
   text-decoration: none;
   color: inherit;
-`
+`;
 const CountBlockLabel = styled(Link)`
   font-size: 12px;
   font-weight: ${ThemeProps.fontWeights.medium};
   text-transform: uppercase;
   color: ${props => props.color};
   text-decoration: none;
-`
+`;
 type Props = {
   data: {
-    label: string,
-    value: number,
-    color: string,
-    link: string,
-    loading: boolean,
-  }[],
-}
+    label: string;
+    value: number;
+    color: string;
+    link: string;
+    loading: boolean;
+  }[];
+};
 @observer
 class DashboardInfoCount extends React.Component<Props> {
   render() {
@@ -78,16 +78,21 @@ class DashboardInfoCount extends React.Component<Props> {
       <Wrapper>
         {this.props.data.map(item => (
           <CountBlock key={item.label}>
-            {
-              !item.value && item.loading ? <LoadingWrapper><StatusImage status="RUNNING" size={48} /></LoadingWrapper>
-                : <CountBlockValue to={item.link}>{item.value}</CountBlockValue>
-            }
-            <CountBlockLabel color={item.color} to={item.link}>{item.label}</CountBlockLabel>
+            {!item.value && item.loading ? (
+              <LoadingWrapper>
+                <StatusImage status="RUNNING" size={48} />
+              </LoadingWrapper>
+            ) : (
+              <CountBlockValue to={item.link}>{item.value}</CountBlockValue>
+            )}
+            <CountBlockLabel color={item.color} to={item.link}>
+              {item.label}
+            </CountBlockLabel>
           </CountBlock>
         ))}
       </Wrapper>
-    )
+    );
   }
 }
 
-export default DashboardInfoCount
+export default DashboardInfoCount;

+ 101 - 80
src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.tsx

@@ -12,30 +12,30 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import * as React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
-import moment from 'moment'
+import * as React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
+import moment from "moment";
 
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
-import InfoIcon from '@src/components/ui/InfoIcon'
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
+import InfoIcon from "@src/components/ui/InfoIcon";
 
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
 
-import type { Licence, LicenceServerStatus } from '@src/@types/Licence'
-import CopyValue from '@src/components/ui/CopyValue'
-import Button from '@src/components/ui/Button'
+import type { Licence, LicenceServerStatus } from "@src/@types/Licence";
+import CopyValue from "@src/components/ui/CopyValue";
+import Button from "@src/components/ui/Button";
 
-import licenceImage from '@src/components/modules/LicenceModule/images/licence'
+import licenceImage from "@src/components/modules/LicenceModule/images/licence";
 
 const Wrapper = styled.div<any>`
   flex-grow: 1;
-`
+`;
 const Title = styled.div<any>`
   font-size: 24px;
   font-weight: ${ThemeProps.fontWeights.light};
   margin-bottom: 12px;
-`
+`;
 const Module = styled.div<any>`
   background: ${ThemePalette.grayscale[0]};
   display: flex;
@@ -43,10 +43,10 @@ const Module = styled.div<any>`
   border-radius: ${ThemeProps.borderRadius};
   padding: 24px 16px 16px 16px;
   height: 232px;
-`
+`;
 const LicenceInfo = styled.div<any>`
   width: 100%;
-`
+`;
 const LicenceError = styled.span`
   p {
     margin: 16px 0 0 0;
@@ -54,41 +54,41 @@ const LicenceError = styled.span`
       margin: 0;
     }
   }
-`
+`;
 const ApplianceId = styled.div`
   display: flex;
   margin-top: 16px;
-`
+`;
 const AddLicenceButtonWrapper = styled.div`
   margin-top: 32px;
   text-align: center;
-`
+`;
 const TopInfo = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
-`
+`;
 const TopInfoText = styled.div`
   margin-bottom: 8px;
   color: ${ThemePalette.grayscale[4]};
-`
+`;
 const TopInfoDate = styled.div<any>`
-  ${ThemeProps.exactWidth('76px')}
-  ${ThemeProps.exactHeight('80px')}
+  ${ThemeProps.exactWidth("76px")}
+  ${ThemeProps.exactHeight("80px")}
   display: flex;
   flex-direction: column;
   ${ThemeProps.boxShadow}
   border-radius: ${ThemeProps.borderRadius};
   overflow: hidden;
-`
+`;
 const TopInfoDateTop = styled.div<any>`
   width: 100%;
   height: 27px;
-  background: linear-gradient(#007AE7, #0044CA);
+  background: linear-gradient(#007ae7, #0044ca);
   color: white;
   text-align: center;
   line-height: 27px;
-`
+`;
 const TopInfoDateBottom = styled.div<any>`
   background: white;
   flex-grow: 1;
@@ -98,46 +98,46 @@ const TopInfoDateBottom = styled.div<any>`
   color: ${ThemePalette.primary};
   font-size: 37px;
   font-weight: ${ThemeProps.fontWeights.extraLight};
-`
+`;
 const Charts = styled.div<any>`
   margin-top: -8px;
-`
+`;
 const ChartRow = styled.div`
   display: flex;
   margin-left: -32px;
   margin-top: 32px;
-`
+`;
 const Chart = styled.div<any>`
   width: 100%;
   margin-left: 32px;
-`
+`;
 const ChartHeader = styled.div<any>`
   display: flex;
   justify-content: space-between;
-`
-const ChartHeaderCurrent = styled.div<any>``
+`;
+const ChartHeaderCurrent = styled.div<any>``;
 const ChartHeaderTotal = styled.div<any>`
   color: ${ThemePalette.grayscale[4]};
-`
+`;
 const ChartBodyWrapper = styled.div<any>`
   height: 8px;
   background: ${ThemePalette.grayscale[2]};
   border-radius: ${ThemeProps.borderRadius};
   margin-top: 4px;
   overflow: hidden;
-`
+`;
 const ChartBody = styled.div<any>`
   width: ${props => props.width}%;
   background: ${props => props.color};
   height: 100%;
-`
+`;
 const Logo = styled.div`
   width: 96px;
   height: 96px;
   margin: 0 auto;
   transform: scale(0.7);
   text-align: center;
-`
+`;
 const LoadingWrapper = styled.div<any>`
   overflow: hidden;
   display: flex;
@@ -145,16 +145,16 @@ const LoadingWrapper = styled.div<any>`
   justify-content: center;
   width: 100%;
   height: 100%;
-`
+`;
 
 type Props = {
-  licence: Licence | null,
-  licenceServerStatus: LicenceServerStatus | null
-  loading: boolean,
-  style: any,
-  licenceError: string | null,
-  onAddClick: () => void,
-}
+  licence: Licence | null;
+  licenceServerStatus: LicenceServerStatus | null;
+  loading: boolean;
+  style: any;
+  licenceError: string | null;
+  onAddClick: () => void;
+};
 @observer
 class DashboardLicence extends React.Component<Props> {
   renderLicenceStatusText(info: Licence): React.ReactNode {
@@ -164,7 +164,7 @@ class DashboardLicence extends React.Component<Props> {
           color: ThemePalette.alert,
           current: info.currentPerformedReplicas,
           total: info.currentAvailableReplicas,
-          label: 'Used Replica',
+          label: "Used Replica",
           info: `The number of replicas consumed over the number of replicas available in
           all currently active licences (including non-activated floating licences)`,
         },
@@ -174,35 +174,42 @@ class DashboardLicence extends React.Component<Props> {
           color: ThemePalette.primary,
           current: info.currentPerformedMigrations,
           total: info.currentAvailableMigrations,
-          label: 'Used Migration',
+          label: "Used Migration",
           info: `The number of migrations consumed over the number of migrations available in
           all currently active licences (including non-activated floating licences)`,
         },
       ],
-    ]
-    const expirationData = moment(info.earliestLicenceExpiryDate)
+    ];
+    const expirationData = moment(info.earliestLicenceExpiryDate);
     return (
       <LicenceInfo>
         <TopInfo>
           <TopInfoText>Expires on</TopInfoText>
           <TopInfoDate>
-            <TopInfoDateTop>{expirationData.format('MMM')} &#39;{expirationData.format('YY')}</TopInfoDateTop>
-            <TopInfoDateBottom>{expirationData.format('DD')}</TopInfoDateBottom>
+            <TopInfoDateTop>
+              {expirationData.format("MMM")} &#39;{expirationData.format("YY")}
+            </TopInfoDateTop>
+            <TopInfoDateBottom>{expirationData.format("DD")}</TopInfoDateBottom>
           </TopInfoDate>
         </TopInfo>
         <Charts>
-          {graphDataRows.map(row => (
-            <ChartRow>
+          {graphDataRows.map((row, i) => (
+            <ChartRow key={i}>
               {row.map(data => (
                 <Chart key={data.label}>
                   <ChartHeader>
                     <ChartHeaderCurrent>
-                      {data.current} {data.current === 1 ? data.label : `${data.label}s`} <InfoIcon marginBottom={-3} text={data.info} />
+                      {data.current}{" "}
+                      {data.current === 1 ? data.label : `${data.label}s`}{" "}
+                      <InfoIcon marginBottom={-3} text={data.info} />
                     </ChartHeaderCurrent>
                     <ChartHeaderTotal>Total {data.total}</ChartHeaderTotal>
                   </ChartHeader>
                   <ChartBodyWrapper>
-                    <ChartBody color={data.color} width={(data.current / data.total) * 100} />
+                    <ChartBody
+                      color={data.color}
+                      width={(data.current / data.total) * 100}
+                    />
                   </ChartBodyWrapper>
                 </Chart>
               ))}
@@ -210,41 +217,51 @@ class DashboardLicence extends React.Component<Props> {
           ))}
         </Charts>
       </LicenceInfo>
-    )
+    );
   }
 
   renderLicenceError() {
     return (
-      <LicenceError>{this.props.licenceError?.split('\n').map(str => <p>{str}</p>)}</LicenceError>
-    )
+      <LicenceError>
+        {this.props.licenceError?.split("\n").map((str, i) => (
+          <p key={i}>{str}</p>
+        ))}
+      </LicenceError>
+    );
   }
 
   renderLicenceExpired(licence: Licence, serverStatus: LicenceServerStatus) {
-    const applianceId = `${licence.applianceId}-licence${serverStatus.supported_licence_versions[0]}`
-    const applianceLabel = applianceId.replace(/(.*-.*-)(.*-.*)(-.*-.*)/, '$1...$3')
+    const applianceId = `${licence.applianceId}-licence${serverStatus.supported_licence_versions[0]}`;
+    const applianceLabel = applianceId.replace(
+      /(.*-.*-)(.*-.*)(-.*-.*)/,
+      "$1...$3"
+    );
     return (
       <LicenceError>
         <p>
-          Please contact Cloudbase Solutions with your Appliance ID
-          in order to obtain a Coriolis® licence.
+          Please contact Cloudbase Solutions with your Appliance ID in order to
+          obtain a Coriolis® licence.
         </p>
         <ApplianceId>
-          Appliance ID: <CopyValue
-            style={{ marginLeft: '8px' }}
+          Appliance ID:{" "}
+          <CopyValue
+            style={{ marginLeft: "8px" }}
             value={applianceId}
             label={applianceLabel}
           />
         </ApplianceId>
         <AddLicenceButtonWrapper>
           <Logo
-            dangerouslySetInnerHTML={
-              { __html: licenceImage(ThemePalette.grayscale[5]) }
-            }
+            dangerouslySetInnerHTML={{
+              __html: licenceImage(ThemePalette.grayscale[5]),
+            }}
           />
-          <Button primary onClick={this.props.onAddClick}>Add Licence</Button>
+          <Button primary onClick={this.props.onAddClick}>
+            Add Licence
+          </Button>
         </AddLicenceButtonWrapper>
       </LicenceError>
-    )
+    );
   }
 
   renderLoading() {
@@ -252,33 +269,37 @@ class DashboardLicence extends React.Component<Props> {
       <LoadingWrapper>
         <StatusImage status="RUNNING" />
       </LoadingWrapper>
-    )
+    );
   }
 
   render() {
-    const licence = this.props.licence
-    let moduleContent = null
+    const licence = this.props.licence;
+    let moduleContent = null;
     if (licence && this.props.licenceServerStatus) {
-      if (new Date(licence.earliestLicenceExpiryDate).getTime() > new Date().getTime()) {
-        moduleContent = this.renderLicenceStatusText(licence)
+      if (
+        new Date(licence.earliestLicenceExpiryDate).getTime() >
+        new Date().getTime()
+      ) {
+        moduleContent = this.renderLicenceStatusText(licence);
       } else {
-        moduleContent = this.renderLicenceExpired(licence, this.props.licenceServerStatus)
+        moduleContent = this.renderLicenceExpired(
+          licence,
+          this.props.licenceServerStatus
+        );
       }
     } else if (this.props.loading) {
-      moduleContent = this.renderLoading()
+      moduleContent = this.renderLoading();
     } else if (this.props.licenceError) {
-      moduleContent = this.renderLicenceError()
+      moduleContent = this.renderLicenceError();
     }
 
     return licence || this.props.loading || this.props.licenceError ? (
       <Wrapper style={this.props.style}>
         <Title>Current Licence</Title>
-        <Module>
-          {moduleContent}
-        </Module>
+        <Module>{moduleContent}</Module>
       </Wrapper>
-    ) : null
+    ) : null;
   }
 }
 
-export default DashboardLicence
+export default DashboardLicence;

+ 112 - 101
src/components/modules/DashboardModule/DashboardPieChart/DashboardPieChart.tsx

@@ -12,16 +12,16 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import * as React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
-import autobind from 'autobind-decorator'
-import { ThemeProps } from '@src/components/Theme'
+import * as React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
+import autobind from "autobind-decorator";
+import { ThemeProps } from "@src/components/Theme";
 
 const Wrapper = styled.div<any>`
   position: relative;
   display: flex;
-`
+`;
 const OuterShadow = styled.div<any>`
   position: absolute;
   top: 0;
@@ -30,7 +30,7 @@ const OuterShadow = styled.div<any>`
   border-radius: 50%;
   ${ThemeProps.boxShadow}
   pointer-events: none;
-`
+`;
 const InnerShadow = styled.div<any>`
   position: absolute;
   top: calc(50% - ${props => props.size}px);
@@ -39,183 +39,194 @@ const InnerShadow = styled.div<any>`
   border-radius: 50%;
   box-shadow: inset rgba(0, 0, 0, 0.1) 0 0 6px 2px;
   pointer-events: none;
-`
-const Canvas = styled.canvas``
+`;
+const Canvas = styled.canvas``;
 
-export type DataItem = { value: number, [prop: string]: any }
+export type DataItem = { value: number; [prop: string]: any };
 type Props = {
-  size: number,
-  data: any[],
+  size: number;
+  data: any[];
   holeStyle?: {
-    radius: number,
-    color: string,
-  },
-  colors: string[],
-  onMouseOver?: (item: DataItem, positionX: number, positionY: number) => void,
-  onMouseLeave?: () => void,
-  customRef?: (ref: HTMLElement) => void,
-}
+    radius: number;
+    color: string;
+  };
+  colors: string[];
+  onMouseOver?: (item: DataItem, positionX: number, positionY: number) => void;
+  onMouseLeave?: () => void;
+  customRef?: (ref: HTMLElement) => void;
+};
 
 @observer
 class DashboardPieChart extends React.Component<Props> {
-  canvas: HTMLCanvasElement | null | undefined
+  canvas: HTMLCanvasElement | null | undefined;
 
-  angles: number[] = []
+  angles: number[] = [];
 
-  topData: DataItem[] = []
+  topData: DataItem[] = [];
 
-  sum: number = 0
+  sum = 0;
 
   componentDidMount() {
-    this.drawChart()
-    const canvas = this.canvas
+    this.drawChart();
+    const canvas = this.canvas;
     if (!canvas) {
-      return
+      return;
     }
-    canvas.addEventListener('mousemove', this.handleMouseMove)
-    canvas.addEventListener('mouseleave', this.handleMouseLeave)
+    canvas.addEventListener("mousemove", this.handleMouseMove);
+    canvas.addEventListener("mouseleave", this.handleMouseLeave);
   }
 
   UNSAFE_componentWillReceiveProps() {
-    this.drawChart()
+    this.drawChart();
   }
 
   componentDidUpdate() {
-    this.drawChart()
+    this.drawChart();
   }
 
   componentWillUnmount() {
-    const canvas = this.canvas
+    const canvas = this.canvas;
     if (!canvas) {
-      return
+      return;
     }
-    canvas.removeEventListener('mousemove', this.handleMouseMove)
-    canvas.removeEventListener('mouseleave', this.handleMouseLeave)
+    canvas.removeEventListener("mousemove", this.handleMouseMove);
+    canvas.removeEventListener("mouseleave", this.handleMouseLeave);
   }
 
   @autobind
   handleMouseMove(evt: MouseEvent) {
-    const canvas = this.canvas
-    const onMouseOver = this.props.onMouseOver
+    const canvas = this.canvas;
+    const onMouseOver = this.props.onMouseOver;
     if (!canvas || !onMouseOver) {
-      return
+      return;
     }
-    const mouseX = evt.offsetX
-    const mouseY = evt.offsetY
-    const item = this.detectHit(mouseX * 2, mouseY * 2)
+    const mouseX = evt.offsetX;
+    const mouseY = evt.offsetY;
+    const item = this.detectHit(mouseX * 2, mouseY * 2);
     if (item) {
-      onMouseOver(item, mouseX, mouseY)
+      onMouseOver(item, mouseX, mouseY);
     } else if (this.props.onMouseLeave) {
-      this.props.onMouseLeave()
+      this.props.onMouseLeave();
     }
   }
 
   @autobind
   handleMouseLeave() {
     if (this.props.onMouseLeave) {
-      this.props.onMouseLeave()
+      this.props.onMouseLeave();
     }
   }
 
   drawChart() {
-    const canvas = this.canvas
+    const canvas = this.canvas;
     if (!canvas) {
-      return
+      return;
     }
-    canvas.style.width = `${this.props.size}px`
-    canvas.style.height = `${this.props.size}px`
+    canvas.style.width = `${this.props.size}px`;
+    canvas.style.height = `${this.props.size}px`;
 
-    this.topData = this.props.data.sort((a, b) => b.value - a.value).slice(0, 6)
-    this.sum = this.topData.reduce((total, item) => total + item.value, 0)
+    this.topData = this.props.data
+      .sort((a, b) => b.value - a.value)
+      .slice(0, 6);
+    this.sum = this.topData.reduce((total, item) => total + item.value, 0);
     if (this.sum === 0) {
-      this.angles = this.topData.map(() => Math.PI * ((1 / this.topData.length) * 2))
+      this.angles = this.topData.map(
+        () => Math.PI * ((1 / this.topData.length) * 2)
+      );
     } else {
-      this.angles = this.topData.map(item => Math.PI * ((item.value / this.sum) * 2))
+      this.angles = this.topData.map(
+        item => Math.PI * ((item.value / this.sum) * 2)
+      );
     }
-    const halfSize = this.props.size / 2
-    const ctx = canvas.getContext('2d')
+    const halfSize = this.props.size / 2;
+    const ctx = canvas.getContext("2d");
     if (!ctx) {
-      return
+      return;
     }
-    ctx.setTransform(1, 0, 0, 1, 0, 0)
-    ctx.clearRect(0, 0, this.props.size * 2, this.props.size * 2)
-    ctx.scale(2, 2)
-    let beginAngle = Math.PI
-    let endAngle = Math.PI
+    ctx.setTransform(1, 0, 0, 1, 0, 0);
+    ctx.clearRect(0, 0, this.props.size * 2, this.props.size * 2);
+    ctx.scale(2, 2);
+    let beginAngle = Math.PI;
+    let endAngle = Math.PI;
     for (let i = 0; i < this.angles.length; i += 1) {
-      beginAngle = endAngle
-      endAngle += this.angles[i]
-
-      ctx.beginPath()
-      ctx.fillStyle = this.props.colors[i % this.props.colors.length]
-      ctx.moveTo(halfSize, halfSize)
-      ctx.arc(halfSize, halfSize, halfSize, beginAngle, endAngle)
-      ctx.fill()
+      beginAngle = endAngle;
+      endAngle += this.angles[i];
+
+      ctx.beginPath();
+      ctx.fillStyle = this.props.colors[i % this.props.colors.length];
+      ctx.moveTo(halfSize, halfSize);
+      ctx.arc(halfSize, halfSize, halfSize, beginAngle, endAngle);
+      ctx.fill();
     }
-    const holeStyle = this.props.holeStyle
+    const holeStyle = this.props.holeStyle;
     if (!holeStyle) {
-      return
+      return;
     }
-    ctx.beginPath()
-    ctx.fillStyle = holeStyle.color
-    ctx.moveTo(halfSize, halfSize)
-    ctx.arc(halfSize, halfSize, holeStyle.radius, 0, 2 * Math.PI)
-    ctx.fill()
+    ctx.beginPath();
+    ctx.fillStyle = holeStyle.color;
+    ctx.moveTo(halfSize, halfSize);
+    ctx.arc(halfSize, halfSize, holeStyle.radius, 0, 2 * Math.PI);
+    ctx.fill();
   }
 
   detectHit(x: number, y: number): any {
-    const canvas = this.canvas
+    const canvas = this.canvas;
     if (!canvas) {
-      return null
+      return null;
     }
 
-    const halfSize = this.props.size / 2
-    const ctx = canvas.getContext('2d')
+    const halfSize = this.props.size / 2;
+    const ctx = canvas.getContext("2d");
     if (!ctx) {
-      return null
+      return null;
     }
-    const holeStyle = this.props.holeStyle
+    const holeStyle = this.props.holeStyle;
     if (holeStyle) {
-      ctx.beginPath()
-      ctx.moveTo(halfSize, halfSize)
-      ctx.arc(halfSize, halfSize, holeStyle.radius, 0, 2 * Math.PI)
+      ctx.beginPath();
+      ctx.moveTo(halfSize, halfSize);
+      ctx.arc(halfSize, halfSize, holeStyle.radius, 0, 2 * Math.PI);
       if (ctx.isPointInPath(x, y)) {
-        return null
+        return null;
       }
     }
 
-    let beginAngle = Math.PI
-    let endAngle = Math.PI
+    let beginAngle = Math.PI;
+    let endAngle = Math.PI;
     for (let i = 0; i < this.angles.length; i += 1) {
-      beginAngle = endAngle
-      endAngle += this.angles[i]
+      beginAngle = endAngle;
+      endAngle += this.angles[i];
 
-      ctx.beginPath()
-      ctx.moveTo(halfSize, halfSize)
-      ctx.arc(halfSize, halfSize, halfSize, beginAngle, endAngle)
+      ctx.beginPath();
+      ctx.moveTo(halfSize, halfSize);
+      ctx.arc(halfSize, halfSize, halfSize, beginAngle, endAngle);
       if (ctx.isPointInPath(x, y)) {
-        return this.topData[i]
+        return this.topData[i];
       }
     }
-    return null
+    return null;
   }
 
   render() {
     return (
-      <Wrapper ref={(ref: HTMLElement) => {
-        if (this.props.customRef) this.props.customRef(ref)
-      }}
+      <Wrapper
+        ref={(ref: HTMLElement) => {
+          if (this.props.customRef) this.props.customRef(ref);
+        }}
       >
         <Canvas
           width={this.props.size * 2}
           height={this.props.size * 2}
-          ref={ref => { this.canvas = ref }}
+          ref={ref => {
+            this.canvas = ref;
+          }}
         />
         <OuterShadow size={this.props.size} />
-        {this.props.holeStyle ? <InnerShadow size={this.props.holeStyle.radius} /> : null}
+        {this.props.holeStyle ? (
+          <InnerShadow size={this.props.holeStyle.radius} />
+        ) : null}
       </Wrapper>
-    )
+    );
   }
 }
 
-export default DashboardPieChart
+export default DashboardPieChart;

+ 137 - 94
src/components/modules/DashboardModule/DashboardTopEndpoints/DashboardTopEndpoints.tsx

@@ -12,58 +12,58 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import * as React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
-import { Link } from 'react-router-dom'
+import * as React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
+import { Link } from "react-router-dom";
 
-import Button from '@src/components/ui/Button'
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
-import EndpointLogos from '@src/components/modules/EndpointModule/EndpointLogos'
-import DashboardPieChart from '@src/components/modules/DashboardModule/DashboardPieChart'
+import Button from "@src/components/ui/Button";
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
+import EndpointLogos from "@src/components/modules/EndpointModule/EndpointLogos";
+import DashboardPieChart from "@src/components/modules/DashboardModule/DashboardPieChart";
 
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
 
-import type { Endpoint } from '@src/@types/Endpoint'
+import type { Endpoint } from "@src/@types/Endpoint";
 
-import { ReplicaItem, MigrationItem, TransferItem } from '@src/@types/MainItem'
-import endpointImage from './images/endpoint.svg'
+import { ReplicaItem, MigrationItem, TransferItem } from "@src/@types/MainItem";
+import endpointImage from "./images/endpoint.svg";
 
 const Wrapper = styled.div<any>`
   flex-grow: 1;
-`
+`;
 const Title = styled.div<any>`
   font-size: 24px;
   font-weight: ${ThemeProps.fontWeights.light};
   margin-bottom: 12px;
-`
+`;
 const Module = styled.div<any>`
   background: ${ThemePalette.grayscale[0]};
   border-radius: ${ThemeProps.borderRadius};
   height: 224px;
   padding: 32px 16px 16px 16px;
-`
+`;
 const ChartWrapper = styled.div<any>`
   position: relative;
   display: flex;
   flex-direction: column;
   align-items: center;
   height: 100%;
-`
+`;
 const LoadingWrapper = styled.div<any>`
   height: 100%;
   display: flex;
   align-items: center;
   justify-content: center;
   overflow: hidden;
-`
+`;
 const Tooltip = styled.div<any>`
   position: absolute;
   width: 208px;
   overflow: hidden;
   border-radius: ${ThemeProps.borderRadius};
-  box-shadow: rgba(0,0,0,0.1) 0 0 6px 1px;
-`
+  box-shadow: rgba(0, 0, 0, 0.1) 0 0 6px 1px;
+`;
 const TooltipHeader = styled.div<any>`
   background: ${ThemePalette.grayscale[3]};
   height: 24px;
@@ -71,7 +71,7 @@ const TooltipHeader = styled.div<any>`
   align-items: center;
   color: white;
   padding: 0 14px;
-`
+`;
 const TooltipBody = styled.div<any>`
   background: ${ThemePalette.black};
   height: 54px;
@@ -79,29 +79,29 @@ const TooltipBody = styled.div<any>`
   align-items: center;
   justify-content: space-between;
   padding: 0 16px;
-`
+`;
 const TooltipRows = styled.div<any>`
   color: white;
   font-size: 10px;
-`
-const TooltipRow = styled.div<any>``
+`;
+const TooltipRow = styled.div<any>``;
 const Legend = styled.div<any>`
   display: flex;
   flex-wrap: wrap;
   margin-left: -8px;
   width: 100%;
-`
+`;
 const LegendItem = styled.div<any>`
   display: flex;
   margin-top: 24px;
   margin-left: 8px;
   width: calc(33% - 8px);
-`
+`;
 const LegendBullet = styled.div<any>`
-  ${ThemeProps.exactSize('8px')}
+  ${ThemeProps.exactSize("8px")}
   border: 2px solid ${props => props.color};
   border-radius: 50%;
-`
+`;
 const LegendLabel = styled(Link)`
   display: block;
   font-size: 10px;
@@ -111,154 +111,189 @@ const LegendLabel = styled(Link)`
   margin-left: 6px;
   text-decoration: none;
   color: inherit;
-`
+`;
 const NoItems = styled.div<any>`
   margin-top: -32px;
   display: flex;
   flex-direction: column;
   align-items: center;
   width: 100%;
-`
+`;
 const EndpointImage = styled.div<any>`
-  ${ThemeProps.exactSize('148px')}
+  ${ThemeProps.exactSize("148px")}
   background: url('${endpointImage}') center no-repeat;
-`
+`;
 const Message = styled.div<any>`
   text-align: center;
   margin-bottom: 32px;
-`
+`;
 
 type GroupedEndpoint = {
-  endpoint: Endpoint,
-  replicasCount: number,
-  migrationsCount: number,
-  value: number,
-}
+  endpoint: Endpoint;
+  replicasCount: number;
+  migrationsCount: number;
+  value: number;
+};
 type Props = {
   // eslint-disable-next-line react/no-unused-prop-types
-  replicas: ReplicaItem[],
+  replicas: ReplicaItem[];
   // eslint-disable-next-line react/no-unused-prop-types
-  migrations: MigrationItem[],
+  migrations: MigrationItem[];
   // eslint-disable-next-line react/no-unused-prop-types
-  endpoints: Endpoint[],
-  style: any,
-  loading: boolean,
-  onNewClick: () => void,
-}
+  endpoints: Endpoint[];
+  style: any;
+  loading: boolean;
+  onNewClick: () => void;
+};
 type State = {
-  tooltipPosition: { x: number, y: number },
-  groupedEndpoint: GroupedEndpoint | null,
-  groupedEndpoints: GroupedEndpoint[],
-}
-const COLORS = ['#280E4C', '#FF2D55', '#FDC02F', '#0044CA', '#39DA55', '#A4AAB5']
+  tooltipPosition: { x: number; y: number };
+  groupedEndpoint: GroupedEndpoint | null;
+  groupedEndpoints: GroupedEndpoint[];
+};
+const COLORS = [
+  "#280E4C",
+  "#FF2D55",
+  "#FDC02F",
+  "#0044CA",
+  "#39DA55",
+  "#A4AAB5",
+];
 @observer
 class DashboardTopEndpoints extends React.Component<Props, State> {
   state: State = {
     tooltipPosition: { x: 0, y: 0 },
     groupedEndpoint: null,
     groupedEndpoints: [],
-  }
+  };
 
-  chartRef: HTMLElement | null | undefined
+  chartRef: HTMLElement | null | undefined;
 
   UNSAFE_componentWillMount() {
-    this.calculateGroupedEndpoints(this.props)
+    this.calculateGroupedEndpoints(this.props);
   }
 
   UNSAFE_componentWillReceiveProps(props: Props) {
-    this.calculateGroupedEndpoints(props)
+    this.calculateGroupedEndpoints(props);
   }
 
   calculateGroupedEndpoints(props: Props) {
-    const groupedEndpoints: GroupedEndpoint[] = []
-    const count = (mainItems: TransferItem[], endpointId: string) => mainItems
-      .filter(r => r.destination_endpoint_id === endpointId
-        || r.origin_endpoint_id === endpointId).length
+    const groupedEndpoints: GroupedEndpoint[] = [];
+    const count = (mainItems: TransferItem[], endpointId: string) =>
+      mainItems.filter(
+        r =>
+          r.destination_endpoint_id === endpointId ||
+          r.origin_endpoint_id === endpointId
+      ).length;
 
     props.endpoints.forEach(endpoint => {
-      const replicasCount = count(props.replicas, endpoint.id)
-      const migrationsCount = count(props.migrations, endpoint.id)
+      const replicasCount = count(props.replicas, endpoint.id);
+      const migrationsCount = count(props.migrations, endpoint.id);
       groupedEndpoints.push({
-        endpoint, replicasCount, migrationsCount, value: replicasCount + migrationsCount,
-      })
-    })
-    this.setState({ groupedEndpoints })
+        endpoint,
+        replicasCount,
+        migrationsCount,
+        value: replicasCount + migrationsCount,
+      });
+    });
+    this.setState({ groupedEndpoints });
   }
 
   handleMouseOver(item: Partial<GroupedEndpoint>, x: number, y: number) {
     if (!this.chartRef) {
-      return
+      return;
     }
-    const canvasCoord = { x, y }
-    const chartBaseCoord = { x: this.chartRef.offsetLeft, y: this.chartRef.offsetTop }
-    const offset = { x: x > 70 ? -224 : 16, y: -32 }
+    const canvasCoord = { x, y };
+    const chartBaseCoord = {
+      x: this.chartRef.offsetLeft,
+      y: this.chartRef.offsetTop,
+    };
+    const offset = { x: x > 70 ? -224 : 16, y: -32 };
     const tooltipPosition = {
       x: canvasCoord.x + chartBaseCoord.x + offset.x,
       y: canvasCoord.y + chartBaseCoord.y + offset.y,
-    }
+    };
     this.setState({
       groupedEndpoint: item as GroupedEndpoint,
       tooltipPosition,
-    })
+    });
   }
 
   handleMouseLeave() {
-    this.setState({ groupedEndpoint: null })
+    this.setState({ groupedEndpoint: null });
   }
 
   renderLegend() {
-    const topData = this.state.groupedEndpoints.sort((a, b) => b.value - a.value).slice(0, 6)
+    const topData = this.state.groupedEndpoints
+      .sort((a, b) => b.value - a.value)
+      .slice(0, 6);
     return (
       <Legend>
         {topData.map((item, i) => (
           <LegendItem key={item.endpoint.id}>
             <LegendBullet color={COLORS[i % COLORS.length]} />
-            <LegendLabel to={`/endpoints/${item.endpoint.id}`}>{item.endpoint.name}</LegendLabel>
+            <LegendLabel to={`/endpoints/${item.endpoint.id}`}>
+              {item.endpoint.name}
+            </LegendLabel>
           </LegendItem>
         ))}
       </Legend>
-    )
+    );
   }
 
   renderTooltip() {
-    const groupedEndpoint = this.state.groupedEndpoint
+    const groupedEndpoint = this.state.groupedEndpoint;
     if (!groupedEndpoint) {
-      return null
+      return null;
     }
 
     return (
-      <Tooltip style={{ top: this.state.tooltipPosition.y, left: this.state.tooltipPosition.x }}>
-        <TooltipHeader>
-          {groupedEndpoint.endpoint.name}
-        </TooltipHeader>
+      <Tooltip
+        style={{
+          top: this.state.tooltipPosition.y,
+          left: this.state.tooltipPosition.x,
+        }}
+      >
+        <TooltipHeader>{groupedEndpoint.endpoint.name}</TooltipHeader>
         <TooltipBody>
-          <EndpointLogos white endpoint={groupedEndpoint.endpoint.type} height={32} />
+          <EndpointLogos
+            white
+            endpoint={groupedEndpoint.endpoint.type}
+            height={32}
+          />
           <TooltipRows>
             <TooltipRow>{groupedEndpoint.replicasCount} Replicas</TooltipRow>
-            <TooltipRow>{groupedEndpoint.migrationsCount} Migrations</TooltipRow>
+            <TooltipRow>
+              {groupedEndpoint.migrationsCount} Migrations
+            </TooltipRow>
             <TooltipRow>{groupedEndpoint.value} Total</TooltipRow>
           </TooltipRows>
         </TooltipBody>
       </Tooltip>
-    )
+    );
   }
 
   renderChart() {
     return (
       <ChartWrapper>
         <DashboardPieChart
-          customRef={ref => { this.chartRef = ref }}
+          customRef={ref => {
+            this.chartRef = ref;
+          }}
           size={144}
           data={this.state.groupedEndpoints}
           colors={COLORS}
           holeStyle={{ radius: 57, color: ThemePalette.grayscale[0] }}
-          onMouseOver={(item, x, y) => { this.handleMouseOver(item, x, y) }}
-          onMouseLeave={() => { this.handleMouseLeave() }}
+          onMouseOver={(item, x, y) => {
+            this.handleMouseOver(item, x, y);
+          }}
+          onMouseLeave={() => {
+            this.handleMouseLeave();
+          }}
         />
         {this.renderLegend()}
         {this.renderTooltip()}
       </ChartWrapper>
-    )
+    );
   }
 
   renderLoading() {
@@ -266,17 +301,23 @@ class DashboardTopEndpoints extends React.Component<Props, State> {
       <LoadingWrapper>
         <StatusImage status="RUNNING" />
       </LoadingWrapper>
-    )
+    );
   }
 
   renderNoData() {
     return (
       <NoItems>
         <EndpointImage />
-        <Message>There are no Cloud Endpoints<br />in this project.</Message>
-        <Button hollow primary transparent onClick={this.props.onNewClick}>New Endpoint</Button>
+        <Message>
+          There are no Cloud Endpoints
+          <br />
+          in this project.
+        </Message>
+        <Button hollow primary transparent onClick={this.props.onNewClick}>
+          New Endpoint
+        </Button>
       </NoItems>
-    )
+    );
   }
 
   render() {
@@ -285,12 +326,14 @@ class DashboardTopEndpoints extends React.Component<Props, State> {
         <Title>Top Endpoints</Title>
         <Module>
           {this.props.loading && this.props.endpoints.length === 0
-            ? this.renderLoading() : this.props.endpoints.length
-              ? this.renderChart() : this.renderNoData()}
+            ? this.renderLoading()
+            : this.props.endpoints.length
+            ? this.renderChart()
+            : this.renderNoData()}
         </Module>
       </Wrapper>
-    )
+    );
   }
 }
 
-export default DashboardTopEndpoints
+export default DashboardTopEndpoints;

+ 44 - 49
src/components/modules/DetailsModule/DetailsContentHeader/DetailsContentHeader.tsx

@@ -12,18 +12,18 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
-import { Link } from 'react-router-dom'
+import React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
+import { Link } from "react-router-dom";
 
-import StatusPill from '@src/components/ui/StatusComponents/StatusPill'
-import ActionDropdown from '@src/components/ui/Dropdowns/ActionDropdown'
-import type { DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
+import StatusPill from "@src/components/ui/StatusComponents/StatusPill";
+import ActionDropdown from "@src/components/ui/Dropdowns/ActionDropdown";
+import type { DropdownAction } from "@src/components/ui/Dropdowns/ActionDropdown";
 
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
 
-import backArrowImage from './images/back-arrow.svg'
+import backArrowImage from "./images/back-arrow.svg";
 
 const Wrapper = styled.div<any>`
   background: ${ThemePalette.grayscale[0]};
@@ -32,73 +32,73 @@ const Wrapper = styled.div<any>`
   align-items: center;
   justify-content: center;
   margin-left: -72px;
-`
+`;
 const BackButton = styled(Link)`
-  ${ThemeProps.exactSize('33px')}
+  ${ThemeProps.exactSize("33px")}
   background: url('${backArrowImage}') no-repeat center;
   cursor: pointer;
   margin-right: 32px;
-`
+`;
 const TypeImage = styled.div<any>`
   min-width: 64px;
   height: 64px;
-  background: url('${props => props.image}') no-repeat center;
+  background: url("${props => props.image}") no-repeat center;
   margin-right: 64px;
-`
+`;
 const Title = styled.div<any>`
   display: flex;
   align-items: center;
   ${ThemeProps.exactWidth(ThemeProps.contentWidth)}
-`
+`;
 const Text = styled.div<any>`
   font-size: 30px;
   font-weight: ${ThemeProps.fontWeights.light};
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-`
+`;
 const Status = styled.div<any>`
   flex-grow: 1;
   text-overflow: ellipsis;
   overflow: hidden;
-`
+`;
 const StatusPills = styled.div<any>`
   display: flex;
   margin-top: 5px;
   & > div {
     margin-right: 16px;
   }
-`
+`;
 const Description = styled.div<any>`
   color: ${ThemePalette.grayscale[4]};
   margin-top: 3px;
-`
+`;
 const MockButton = styled.div<any>`
   ${ThemeProps.exactWidth(`${ThemeProps.inputSizes.regular.width}px`)}
-`
+`;
 
 type Props = {
-  dropdownActions?: DropdownAction[],
-  backLink: string,
-  typeImage?: string,
-  alertInfoPill?: boolean,
-  primaryInfoPill?: boolean,
-  statusPill?: string,
-  statusLabel?: string,
-  itemTitle?: string | null
-  itemType?: string
-  itemDescription?: string
-  largeDropdownActionItems?: boolean
-}
+  dropdownActions?: DropdownAction[];
+  backLink: string;
+  typeImage?: string;
+  alertInfoPill?: boolean;
+  primaryInfoPill?: boolean;
+  statusPill?: string;
+  statusLabel?: string;
+  itemTitle?: string | null;
+  itemType?: string;
+  itemDescription?: string;
+  largeDropdownActionItems?: boolean;
+};
 @observer
 class DetailsContentHeader extends React.Component<Props> {
   renderStatusPill() {
     if (!this.props.statusPill) {
-      return null
+      return null;
     }
-    let statusLabel = this.props.statusPill
+    let statusLabel = this.props.statusPill;
     if (this.props.statusLabel) {
-      statusLabel = this.props.statusLabel
+      statusLabel = this.props.statusLabel;
     }
     return (
       <StatusPills>
@@ -108,36 +108,31 @@ class DetailsContentHeader extends React.Component<Props> {
           alert={this.props.alertInfoPill}
           primary={this.props.primaryInfoPill}
         />
-        <StatusPill
-          status={this.props.statusPill}
-          label={statusLabel || ''}
-        />
+        <StatusPill status={this.props.statusPill} label={statusLabel || ""} />
       </StatusPills>
-    )
+    );
   }
 
   renderButton() {
     if (!this.props.dropdownActions) {
-      return <MockButton />
+      return <MockButton />;
     }
 
     return (
       <ActionDropdown
         actions={this.props.dropdownActions}
         largeItems={this.props.largeDropdownActionItems}
-        style={{ marginLeft: '32px' }}
+        style={{ marginLeft: "32px" }}
       />
-    )
+    );
   }
 
   renderDescription() {
     if (!this.props.itemDescription) {
-      return null
+      return null;
     }
 
-    return (
-      <Description>{this.props.itemDescription}</Description>
-    )
+    return <Description>{this.props.itemDescription}</Description>;
   }
 
   render() {
@@ -154,8 +149,8 @@ class DetailsContentHeader extends React.Component<Props> {
           {this.renderButton()}
         </Title>
       </Wrapper>
-    )
+    );
   }
 }
 
-export default DetailsContentHeader
+export default DetailsContentHeader;

+ 1 - 2
src/components/modules/DetailsModule/DetailsContentHeader/package.json

@@ -2,6 +2,5 @@
   "name": "DetailsContentHeader",
   "version": "0.0.0",
   "private": true,
-  "main":"./DetailsContentHeader.tsx"
+  "main": "./DetailsContentHeader.tsx"
 }
-

+ 22 - 36
src/components/modules/DetailsModule/DetailsContentHeader/story.tsx

@@ -14,49 +14,35 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 /* eslint-disable react/jsx-props-no-spreading */
 
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import DetailsContentHeader from '.'
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import DetailsContentHeader from ".";
 
 const item = {
-  origin_endpoint_id: 'openstack',
-  destination_endpoint_id: 'azure',
-  instances: ['The instance title'],
-  executions: [{ status: 'COMPLETED', created_at: new Date() }],
-}
-const props: any = {}
-storiesOf('DetailsContentHeader', module)
-  .add('default', () => (
+  origin_endpoint_id: "openstack",
+  destination_endpoint_id: "azure",
+  instances: ["The instance title"],
+  executions: [{ status: "COMPLETED", created_at: new Date() }],
+};
+const props: any = {};
+storiesOf("DetailsContentHeader", module)
+  .add("default", () => <DetailsContentHeader item={item} {...props} />)
+  .add("action button", () => <DetailsContentHeader item={item} {...props} />)
+  .add("running", () => (
     <DetailsContentHeader
-      item={item}
+      item={{
+        ...item,
+        executions: [{ ...item.executions[0], status: "RUNNING" }],
+      }}
       {...props}
     />
   ))
-  .add('action button', () => (
+  .add("description", () => (
     <DetailsContentHeader
-      item={item}
+      item={{ ...item, executions: null, description: "Description text" }}
       {...props}
-
-    />
-  ))
-  .add('running', () => (
-    <DetailsContentHeader
-      item={{ ...item, executions: [{ ...item.executions[0], status: 'RUNNING' }] }}
-      {...props}
-
-    />
-  ))
-  .add('description', () => (
-    <DetailsContentHeader
-      item={{ ...item, executions: null, description: 'Description text' }}
-      {...props}
-    />
-  ))
-  .add('alert pill', () => (
-    <DetailsContentHeader
-      item={item}
-      alertInfoPill
-      {...props}
-
     />
   ))
+  .add("alert pill", () => (
+    <DetailsContentHeader item={item} alertInfoPill {...props} />
+  ));

+ 52 - 53
src/components/modules/DetailsModule/DetailsContentHeader/test.tsx

@@ -12,40 +12,38 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { shallow } from 'enzyme'
-import TW from '@src/utils/TestWrapper'
-import DetailsContentHeader from '.'
+import React from "react";
+import { shallow } from "enzyme";
+import TW from "@src/utils/TestWrapper";
+import DetailsContentHeader from ".";
 
-const wrap = props => new TW(shallow(
+const wrap = props =>
+  new TW(shallow(<DetailsContentHeader {...props} />), "dcHeader");
 
-  <DetailsContentHeader {...props} />
-), 'dcHeader')
+const item = {
+  origin_endpoint_id: "openstack",
+  destination_endpoint_id: "azure",
+  instances: ["The instance title"],
+  type: "item type",
+  executions: [{ status: "COMPLETED", created_at: new Date() }],
+};
 
-let item = {
-  origin_endpoint_id: 'openstack',
-  destination_endpoint_id: 'azure',
-  instances: ['The instance title'],
-  type: 'item type',
-  executions: [{ status: 'COMPLETED', created_at: new Date() }],
-}
+describe("DetailsContentHeader Component", () => {
+  it("renders title", () => {
+    const wrapper = wrap({ item });
+    expect(wrapper.findText("title")).toBe(item.instances[0]);
+  });
 
-describe('DetailsContentHeader Component', () => {
-  it('renders title', () => {
-    let wrapper = wrap({ item })
-    expect(wrapper.findText('title')).toBe(item.instances[0])
-  })
+  it("renders with no action button", () => {
+    const wrapper = wrap({ item });
+    expect(wrapper.find("actionButton").length).toBe(0);
+    expect(wrapper.find("cancelButton").length).toBe(0);
+  });
 
-  it('renders with no action button', () => {
-    let wrapper = wrap({ item })
-    expect(wrapper.find('actionButton').length).toBe(0)
-    expect(wrapper.find('cancelButton').length).toBe(0)
-  })
-
-  it('renders with action button, if there are dropdown actions', () => {
-    let wrapper = wrap({ item, dropdownActions: [] })
-    expect(wrapper.find('actionButton').length).toBe(1)
-  })
+  it("renders with action button, if there are dropdown actions", () => {
+    const wrapper = wrap({ item, dropdownActions: [] });
+    expect(wrapper.find("actionButton").length).toBe(1);
+  });
 
   // it('dispatches back button click', () => {
   //   let onBackButonClick = sinon.spy()
@@ -54,30 +52,31 @@ describe('DetailsContentHeader Component', () => {
   //   expect(onBackButonClick.called).toBe(true)
   // })
 
-  it('renders correct INFO pill', () => {
-    let wrapper = wrap({ item, primaryInfoPill: true })
-    expect(wrapper.find('infoPill').prop('primary')).toBe(true)
-    expect(wrapper.find('infoPill').prop('label')).toBe('ITEM TYPE')
-    expect(wrapper.find('infoPill').prop('alert')).toBe(undefined)
-
-    wrapper = wrap({ item, alertInfoPill: true })
-    expect(wrapper.find('infoPill').prop('alert')).toBe(true)
-  })
-
-  it('renders correct STATUS pill', () => {
-    let wrapper = wrap({ item })
-    expect(wrapper.findPartialId('statusPill-').prop('status')).toBe('COMPLETED')
-    let newItem = { ...item, executions: [...item.executions] }
-    newItem.executions.push({ status: 'RUNNING', created_at: new Date() })
-    wrapper = wrap({ item: newItem })
-    expect(wrapper.findPartialId('statusPill-').prop('status')).toBe('RUNNING')
-  })
-
-  it('renders item description', () => {
-    let wrapper = wrap({ item: { ...item, description: 'item description' } })
-    expect(wrapper.findText('description')).toBe('item description')
-  })
-})
+  it("renders correct INFO pill", () => {
+    let wrapper = wrap({ item, primaryInfoPill: true });
+    expect(wrapper.find("infoPill").prop("primary")).toBe(true);
+    expect(wrapper.find("infoPill").prop("label")).toBe("ITEM TYPE");
+    expect(wrapper.find("infoPill").prop("alert")).toBe(undefined);
 
+    wrapper = wrap({ item, alertInfoPill: true });
+    expect(wrapper.find("infoPill").prop("alert")).toBe(true);
+  });
 
+  it("renders correct STATUS pill", () => {
+    let wrapper = wrap({ item });
+    expect(wrapper.findPartialId("statusPill-").prop("status")).toBe(
+      "COMPLETED"
+    );
+    const newItem = { ...item, executions: [...item.executions] };
+    newItem.executions.push({ status: "RUNNING", created_at: new Date() });
+    wrapper = wrap({ item: newItem });
+    expect(wrapper.findPartialId("statusPill-").prop("status")).toBe("RUNNING");
+  });
 
+  it("renders item description", () => {
+    const wrapper = wrap({
+      item: { ...item, description: "item description" },
+    });
+    expect(wrapper.findText("description")).toBe("item description");
+  });
+});

+ 57 - 46
src/components/modules/DetailsModule/DetailsPageHeader/DetailsPageHeader.tsx

@@ -12,101 +12,106 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { Link } from 'react-router-dom'
-import styled from 'styled-components'
-import { observer } from 'mobx-react'
+import React from "react";
+import { Link } from "react-router-dom";
+import styled from "styled-components";
+import { observer } from "mobx-react";
 
-import NavigationMini from '@src/components/modules/NavigationModule/NavigationMini'
-import NotificationDropdown from '@src/components/ui/Dropdowns/NotificationDropdown'
-import UserDropdown from '@src/components/ui/Dropdowns/UserDropdown'
-import AboutModal from '@src/components/smart/AboutModal'
+import NavigationMini from "@src/components/modules/NavigationModule/NavigationMini";
+import NotificationDropdown from "@src/components/ui/Dropdowns/NotificationDropdown";
+import UserDropdown from "@src/components/ui/Dropdowns/UserDropdown";
+import AboutModal from "@src/components/smart/AboutModal";
 
-import type { User as UserType } from '@src/@types/User'
+import type { User as UserType } from "@src/@types/User";
 
-import notificationStore from '@src/stores/NotificationStore'
+import notificationStore from "@src/stores/NotificationStore";
 
-import backgroundImage from './images/star-bg.jpg'
-import logoImage from './images/logo.svg'
+import backgroundImage from "./images/star-bg.jpg";
+import logoImage from "./images/logo.svg";
 
 const Wrapper = styled.div<any>`
   display: flex;
   height: 64px;
-  background: url('${backgroundImage}');
+  background: url("${backgroundImage}");
   align-items: center;
   padding-right: 22px;
   justify-content: space-between;
-`
+`;
 const Logo = styled(Link)`
   width: 240px;
   height: 48px;
-  background: url('${logoImage}') no-repeat;
+  background: url("${logoImage}") no-repeat;
   cursor: pointer;
-`
+`;
 const UserDropdownStyled = styled(UserDropdown)`
   margin-left: 16px;
-`
+`;
 const Menu = styled.div<any>`
   display: flex;
   align-items: center;
-`
+`;
 const User = styled.div<any>`
   display: flex;
   align-items: center;
-`
+`;
 type State = {
-  showAbout: boolean,
-}
+  showAbout: boolean;
+};
 type Props = {
-  user?: UserType | null,
-  onUserItemClick: (userItem: { label: React.ReactNode, value: string }) => void,
-  testMode?: boolean,
-}
+  user?: UserType | null;
+  onUserItemClick: (userItem: {
+    label: React.ReactNode;
+    value: string;
+  }) => void;
+  testMode?: boolean;
+};
 
 @observer
 class DetailsPageHeader extends React.Component<Props, State> {
   state = {
     showAbout: false,
-  }
+  };
 
-  pollTimeout: number | undefined
+  pollTimeout: number | undefined;
 
-  stopPolling!: boolean
+  stopPolling!: boolean;
 
   UNSAFE_componentWillMount() {
     if (this.props.testMode) {
-      return
+      return;
     }
-    this.stopPolling = false
-    this.pollData(true)
+    this.stopPolling = false;
+    this.pollData(true);
   }
 
   componentWillUnmount() {
-    clearTimeout(this.pollTimeout)
-    this.stopPolling = true
+    clearTimeout(this.pollTimeout);
+    this.stopPolling = true;
   }
 
   handleNotificationsClose() {
-    notificationStore.saveSeen()
+    notificationStore.saveSeen();
   }
 
-  handleUserItemClick(item: { label: React.ReactNode, value: string }) {
+  handleUserItemClick(item: { label: React.ReactNode; value: string }) {
     switch (item.value) {
-      case 'about':
-        this.setState({ showAbout: true })
-        break
+      case "about":
+        this.setState({ showAbout: true });
+        break;
       default:
-        this.props.onUserItemClick(item)
+        this.props.onUserItemClick(item);
     }
   }
 
   async pollData(showLoading?: boolean) {
     if (this.stopPolling) {
-      return
+      return;
     }
 
-    await notificationStore.loadData(showLoading)
-    this.pollTimeout = window.setTimeout(() => { this.pollData() }, 15000)
+    await notificationStore.loadData(showLoading);
+    this.pollTimeout = window.setTimeout(() => {
+      this.pollData();
+    }, 15000);
   }
 
   render() {
@@ -125,15 +130,21 @@ class DetailsPageHeader extends React.Component<Props, State> {
           <UserDropdownStyled
             white
             user={this.props.user}
-            onItemClick={item => { this.handleUserItemClick(item) }}
+            onItemClick={item => {
+              this.handleUserItemClick(item);
+            }}
           />
         </User>
         {this.state.showAbout ? (
-          <AboutModal onRequestClose={() => { this.setState({ showAbout: false }) }} />
+          <AboutModal
+            onRequestClose={() => {
+              this.setState({ showAbout: false });
+            }}
+          />
         ) : null}
       </Wrapper>
-    )
+    );
   }
 }
 
-export default DetailsPageHeader
+export default DetailsPageHeader;

+ 1 - 2
src/components/modules/DetailsModule/DetailsPageHeader/package.json

@@ -2,6 +2,5 @@
   "name": "DetailsPageHeader",
   "version": "0.0.0",
   "private": true,
-  "main":"./DetailsPageHeader.tsx"
+  "main": "./DetailsPageHeader.tsx"
 }
-

+ 40 - 41
src/components/modules/DetailsModule/DetailsPageHeader/test.tsx

@@ -12,46 +12,45 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TW from '@src/utils/TestWrapper'
-import type { User } from '@src/@types/User'
-import DetailsPageHeader from '.'
+import React from "react";
+import { shallow } from "enzyme";
+import sinon from "sinon";
+import TW from "@src/utils/TestWrapper";
+import type { User } from "@src/@types/User";
+import DetailsPageHeader from ".";
 
 type Props = {
-  user?: User | null,
-}
-
-const wrap = (props: Props) => new TW(shallow(
-  <DetailsPageHeader
-    onUserItemClick={() => { }}
-    testMode
-    {...props}
-  />
-), 'dpHeader')
-
-let user = {
-  name: 'User name',
-  email: 'email@email.com',
-  id: 'user',
-  project: { id: '', name: '' },
-}
-
-describe('DetailsPageHeader Component', () => {
-  it('renders with given user', () => {
-    let wrapper = wrap({ user })
-    expect(wrapper.find('userDropdown').prop('user').name).toBe(user.name)
-    expect(wrapper.find('userDropdown').prop('user').email).toBe(user.email)
-  })
-
-  it('dispatches user item click', () => {
-    let onUserItemClick = sinon.spy()
-    let wrapper = wrap({ user, onUserItemClick })
-    wrapper.find('userDropdown').simulate('itemClick', { value: '', label: '' })
-    expect(onUserItemClick.called).toBe(true)
-  })
-})
-
-
-
+  user?: User | null;
+};
+
+const wrap = (props: Props) =>
+  new TW(
+    shallow(
+      <DetailsPageHeader onUserItemClick={() => {}} testMode {...props} />
+    ),
+    "dpHeader"
+  );
+
+const user = {
+  name: "User name",
+  email: "email@email.com",
+  id: "user",
+  project: { id: "", name: "" },
+};
+
+describe("DetailsPageHeader Component", () => {
+  it("renders with given user", () => {
+    const wrapper = wrap({ user });
+    expect(wrapper.find("userDropdown").prop("user").name).toBe(user.name);
+    expect(wrapper.find("userDropdown").prop("user").email).toBe(user.email);
+  });
+
+  it("dispatches user item click", () => {
+    const onUserItemClick = sinon.spy();
+    const wrapper = wrap({ user, onUserItemClick });
+    wrapper
+      .find("userDropdown")
+      .simulate("itemClick", { value: "", label: "" });
+    expect(onUserItemClick.called).toBe(true);
+  });
+});

+ 246 - 178
src/components/modules/EndpointModule/ChooseProvider/ChooseProvider.tsx

@@ -12,63 +12,64 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
+import React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
 
-import notificationStore from '@src/stores/NotificationStore'
+import notificationStore from "@src/stores/NotificationStore";
 
-import EndpointLogos from '@src/components/modules/EndpointModule/EndpointLogos'
-import Button from '@src/components/ui/Button'
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
+import EndpointLogos from "@src/components/modules/EndpointModule/EndpointLogos";
+import Button from "@src/components/ui/Button";
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
 
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import FileUtils from '@src/utils/FileUtils'
-import configLoader from '@src/utils/Config'
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
+import FileUtils from "@src/utils/FileUtils";
+import configLoader from "@src/utils/Config";
 
-import type { FileContent } from '@src/utils/FileUtils'
-import type { Endpoint, MultiValidationItem } from '@src/@types/Endpoint'
+import type { FileContent } from "@src/utils/FileUtils";
+import type { Endpoint, MultiValidationItem } from "@src/@types/Endpoint";
 
-import { ProviderTypes } from '@src/@types/Providers'
-import { Region } from '@src/@types/Region'
-import MultipleUploadedEndpoints from './MultipleUploadedEndpoints'
+import { ProviderTypes } from "@src/@types/Providers";
+import { Region } from "@src/@types/Region";
+import MultipleUploadedEndpoints from "./MultipleUploadedEndpoints";
 
 const Wrapper = styled.div<any>`
   display: flex;
   min-height: 0;
   padding: 22px 0 32px 0;
   text-align: center;
-`
+`;
 const Providers = styled.div`
   min-height: 0;
   display: flex;
   flex-direction: column;
   align-items: center;
-`
+`;
 const Logos = styled.div<any>`
   display: flex;
   flex-wrap: wrap;
   overflow: auto;
   min-height: 0;
   flex-grow: 1;
-`
+`;
 const Upload = styled.div<any>`
-  border: 1px dashed ${props => (props.highlight ? ThemePalette.primary : 'white')};
+  border: 1px dashed
+    ${props => (props.highlight ? ThemePalette.primary : "white")};
   margin: 0 32px 16px 32px;
   padding: 16px;
-`
+`;
 const UploadMessage = styled.div<any>`
   color: ${ThemePalette.grayscale[3]};
-`
+`;
 const UploadMessageButton = styled.span`
   color: ${ThemePalette.primary};
   cursor: pointer;
-`
+`;
 const FakeFileInput = styled.input`
   position: absolute;
   opacity: 0;
   top: -99999px;
-`
+`;
 const EndpointLogosStyled = styled(EndpointLogos)`
   transform: scale(0.67);
   transition: all ${ThemeProps.animations.swift};
@@ -76,265 +77,319 @@ const EndpointLogosStyled = styled(EndpointLogos)`
   &:hover {
     transform: scale(0.7);
   }
-`
+`;
 const LoadingWrapper = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
   margin: 32px 0;
   flex-grow: 1;
-`
+`;
 const LoadingText = styled.div<any>`
   font-size: 18px;
   margin-top: 32px;
-`
+`;
 type Props = {
-  providers: ProviderTypes[],
-  regions: Region[]
-  onCancelClick: () => void,
-  onProviderClick: (provider: ProviderTypes) => void,
-  onUploadEndpoint: (endpoint: Endpoint) => void,
-  loading: boolean,
-  onValidateMultipleEndpoints: (endpoints: Endpoint[]) => void,
-  onResizeUpdate?: () => void,
-  multiValidating: boolean,
-  multiValidation: MultiValidationItem[],
-  onRemoveEndpoint: (endpoint: Endpoint) => void,
-  onResetValidation: () => void,
-}
+  providers: ProviderTypes[];
+  regions: Region[];
+  onCancelClick: () => void;
+  onProviderClick: (provider: ProviderTypes) => void;
+  onUploadEndpoint: (endpoint: Endpoint) => void;
+  loading: boolean;
+  onValidateMultipleEndpoints: (endpoints: Endpoint[]) => void;
+  onResizeUpdate?: () => void;
+  multiValidating: boolean;
+  multiValidation: MultiValidationItem[];
+  onRemoveEndpoint: (endpoint: Endpoint) => void;
+  onResetValidation: () => void;
+};
 type State = {
-  highlightDropzone: boolean,
-  multipleUploadedEndpoints: (Endpoint | string)[],
-  invalidRegionsEndpointIds: { id: string, regions: string[] }[],
-}
+  highlightDropzone: boolean;
+  multipleUploadedEndpoints: (Endpoint | string)[];
+  invalidRegionsEndpointIds: { id: string; regions: string[] }[];
+};
 @observer
 class ChooseProvider extends React.Component<Props, State> {
   state: State = {
     highlightDropzone: false,
     multipleUploadedEndpoints: [],
     invalidRegionsEndpointIds: [],
-  }
+  };
 
-  fileInput: HTMLElement | null | undefined
+  fileInput: HTMLElement | null | undefined;
 
-  dragDropListeners: { type: string, listener: (e: any) => any }[] = []
+  dragDropListeners: { type: string; listener: (e: any) => any }[] = [];
 
   UNSAFE_componentWillMount() {
-    setTimeout(() => { this.addDragAndDrop() }, 1000)
+    setTimeout(() => {
+      this.addDragAndDrop();
+    }, 1000);
   }
 
   componentDidUpdate(_: Props, prevState: State) {
-    if (prevState.multipleUploadedEndpoints.length !== this.state.multipleUploadedEndpoints.length
-      && this.props.onResizeUpdate) {
-      this.props.onResizeUpdate()
+    if (
+      prevState.multipleUploadedEndpoints.length !==
+        this.state.multipleUploadedEndpoints.length &&
+      this.props.onResizeUpdate
+    ) {
+      this.props.onResizeUpdate();
     }
   }
 
   componentWillUnmount() {
-    this.removeDragDrop()
+    this.removeDragDrop();
   }
 
   addDragAndDrop() {
-    this.dragDropListeners = [{
-      type: 'dragenter',
-      listener: e => {
-        this.setState({ highlightDropzone: true })
-        e.dataTransfer.dropEffect = 'copy'
-        e.preventDefault()
+    this.dragDropListeners = [
+      {
+        type: "dragenter",
+        listener: e => {
+          this.setState({ highlightDropzone: true });
+          e.dataTransfer.dropEffect = "copy";
+          e.preventDefault();
+        },
       },
-    }, {
-      type: 'dragover',
-      listener: e => {
-        e.dataTransfer.dropEffect = 'copy'
-        e.preventDefault()
+      {
+        type: "dragover",
+        listener: e => {
+          e.dataTransfer.dropEffect = "copy";
+          e.preventDefault();
+        },
       },
-    }, {
-      type: 'dragleave',
-      listener: e => {
-        if (!e.clientX && !e.clientY) {
-          this.setState({ highlightDropzone: false })
-        }
+      {
+        type: "dragleave",
+        listener: e => {
+          if (!e.clientX && !e.clientY) {
+            this.setState({ highlightDropzone: false });
+          }
+        },
       },
-    }, {
-      type: 'drop',
-      listener: async e => {
-        e.preventDefault()
-        this.setState({ highlightDropzone: false })
-        const filesContents = await FileUtils.readContentFromFileList(e.dataTransfer.files)
-        if (filesContents.length === 1) {
-          this.processOneFileContent(filesContents[0].content)
-        } else {
-          this.processMultipleFilesContents(filesContents)
-        }
+      {
+        type: "drop",
+        listener: async e => {
+          e.preventDefault();
+          this.setState({ highlightDropzone: false });
+          const filesContents = await FileUtils.readContentFromFileList(
+            e.dataTransfer.files
+          );
+          if (filesContents.length === 1) {
+            this.processOneFileContent(filesContents[0].content);
+          } else {
+            this.processMultipleFilesContents(filesContents);
+          }
+        },
       },
-    }]
+    ];
 
     this.dragDropListeners.forEach(l => {
-      window.addEventListener(l.type, l.listener)
-    })
+      window.addEventListener(l.type, l.listener);
+    });
   }
 
   removeDragDrop() {
     this.dragDropListeners.forEach(l => {
-      window.removeEventListener(l.type, l.listener)
-    })
-    this.dragDropListeners = []
+      window.removeEventListener(l.type, l.listener);
+    });
+    this.dragDropListeners = [];
   }
 
-  parseEndpoint(content: string, skipAlert?: boolean): { endpoint: Endpoint, unidentRegions: string[] } {
-    const endpoint: Endpoint = JSON.parse(content)
-    if (!endpoint.name || !endpoint.type || !this.props.providers.find(p => p === endpoint.type)) {
-      throw new Error()
+  parseEndpoint(
+    content: string,
+    skipAlert?: boolean
+  ): { endpoint: Endpoint; unidentRegions: string[] } {
+    const endpoint: Endpoint = JSON.parse(content);
+    if (
+      !endpoint.name ||
+      !endpoint.type ||
+      !this.props.providers.find(p => p === endpoint.type)
+    ) {
+      throw new Error();
     }
-    delete (endpoint as any).id
-    const unidentRegions: string[] = []
+    delete (endpoint as any).id;
+    const unidentRegions: string[] = [];
 
     if (endpoint.mapped_regions?.length) {
-      endpoint.mapped_regions = endpoint.mapped_regions.map(nameId => {
-        const region = this.props.regions.find(r => r.id === nameId || r.name === nameId)
-        if (region) {
-          return region.id
-        }
-        unidentRegions.push(nameId)
-        return null
-      }).filter((item: string | null): item is string => Boolean(item))
+      endpoint.mapped_regions = endpoint.mapped_regions
+        .map(nameId => {
+          const region = this.props.regions.find(
+            r => r.id === nameId || r.name === nameId
+          );
+          if (region) {
+            return region.id;
+          }
+          unidentRegions.push(nameId);
+          return null;
+        })
+        .filter((item: string | null): item is string => Boolean(item));
       if (unidentRegions.length && !skipAlert) {
-        notificationStore.alert(`${unidentRegions.length} Coriolis Region${unidentRegions.length > 1 ? 's' : ''} couldn't be mapped`, 'warning')
+        notificationStore.alert(
+          `${unidentRegions.length} Coriolis Region${
+            unidentRegions.length > 1 ? "s" : ""
+          } couldn't be mapped`,
+          "warning"
+        );
       }
     }
-    return { endpoint, unidentRegions }
+    return { endpoint, unidentRegions };
   }
 
   processOneFileContent(content: string) {
-    this.props.onResetValidation()
+    this.props.onResetValidation();
     try {
-      const { endpoint } = this.parseEndpoint(content)
-      this.chooseEndpoint(endpoint)
+      const { endpoint } = this.parseEndpoint(content);
+      this.chooseEndpoint(endpoint);
     } catch (err) {
-      notificationStore.alert('Invalid .endpoint file', 'error')
+      notificationStore.alert("Invalid .endpoint file", "error");
     }
   }
 
   processMultipleFilesContents(filesContents: FileContent[]) {
-    this.props.onResetValidation()
-    const uniqueNames: { [prop: string]: number } = {}
-    const invalidRegionsEndpointIds: { id: string, regions: string[] }[] = []
+    this.props.onResetValidation();
+    const uniqueNames: { [prop: string]: number } = {};
+    const invalidRegionsEndpointIds: { id: string; regions: string[] }[] = [];
 
     const endpoints = filesContents.map(fileContent => {
       try {
-        const { endpoint, unidentRegions } = this.parseEndpoint(fileContent.content, true)
-        const key = `${endpoint.type}${endpoint.name}`
+        const { endpoint, unidentRegions } = this.parseEndpoint(
+          fileContent.content,
+          true
+        );
+        const key = `${endpoint.type}${endpoint.name}`;
         if (uniqueNames[key] === undefined) {
-          uniqueNames[key] = 0
+          uniqueNames[key] = 0;
         } else {
-          uniqueNames[key] += 1
-          endpoint.name = `${endpoint.name} (${uniqueNames[key]})`
+          uniqueNames[key] += 1;
+          endpoint.name = `${endpoint.name} (${uniqueNames[key]})`;
         }
         if (unidentRegions.length) {
-          invalidRegionsEndpointIds.push({ id: `${endpoint.type}${endpoint.name}`, regions: unidentRegions })
+          invalidRegionsEndpointIds.push({
+            id: `${endpoint.type}${endpoint.name}`,
+            regions: unidentRegions,
+          });
         }
-        return endpoint
+        return endpoint;
       } catch (err) {
-        return fileContent.name
+        return fileContent.name;
       }
-    })
+    });
 
-    const sortPriority = configLoader.config.providerSortPriority
+    const sortPriority = configLoader.config.providerSortPriority;
     endpoints.sort((a, b) => {
-      if (typeof a === 'string' && typeof b === 'string') {
-        return a.localeCompare(b)
+      if (typeof a === "string" && typeof b === "string") {
+        return a.localeCompare(b);
       }
-      if (typeof a === 'string') {
-        return 1
+      if (typeof a === "string") {
+        return 1;
       }
-      if (typeof b === 'string') {
-        return -1
+      if (typeof b === "string") {
+        return -1;
       }
       if (sortPriority[a.type] && sortPriority[b.type]) {
-        return (sortPriority[a.type] - sortPriority[b.type]) || a.type.localeCompare(b.type)
+        return (
+          sortPriority[a.type] - sortPriority[b.type] ||
+          a.type.localeCompare(b.type)
+        );
       }
       if (sortPriority[a.type]) {
-        return -1
+        return -1;
       }
       if (sortPriority[b.type]) {
-        return 1
+        return 1;
       }
-      return a.type.localeCompare(b.type)
-    })
+      return a.type.localeCompare(b.type);
+    });
 
     this.setState({
       multipleUploadedEndpoints: endpoints,
       invalidRegionsEndpointIds,
-    })
+    });
   }
 
   chooseEndpoint(endpoint: Endpoint) {
-    this.props.onUploadEndpoint(endpoint)
+    this.props.onUploadEndpoint(endpoint);
   }
 
   async handleFileUpload(files: FileList | null) {
-    const filesContents = await FileUtils.readContentFromFileList(files)
+    const filesContents = await FileUtils.readContentFromFileList(files);
     if (filesContents.length === 1) {
-      this.processOneFileContent(filesContents[0].content)
+      this.processOneFileContent(filesContents[0].content);
     } else {
-      this.processMultipleFilesContents(filesContents)
+      this.processMultipleFilesContents(filesContents);
     }
   }
 
   handleRemoveUploadedEndpoint(endpoint: Endpoint | string, isAdded: boolean) {
     this.setState(prevState => {
-      const multipleUploadedEndpoints = prevState.multipleUploadedEndpoints.filter(e => {
-        if (typeof e === 'string' && typeof endpoint === 'string') {
-          return e !== endpoint
-        }
-        if (typeof e !== 'string' && typeof endpoint !== 'string') {
-          return e.name !== endpoint.name || e.type !== endpoint.type
-        }
-        return true
-      })
-      if (isAdded && typeof endpoint !== 'string') {
-        this.props.onRemoveEndpoint(endpoint)
+      const multipleUploadedEndpoints =
+        prevState.multipleUploadedEndpoints.filter(e => {
+          if (typeof e === "string" && typeof endpoint === "string") {
+            return e !== endpoint;
+          }
+          if (typeof e !== "string" && typeof endpoint !== "string") {
+            return e.name !== endpoint.name || e.type !== endpoint.type;
+          }
+          return true;
+        });
+      if (isAdded && typeof endpoint !== "string") {
+        this.props.onRemoveEndpoint(endpoint);
       }
-      return { multipleUploadedEndpoints }
-    })
+      return { multipleUploadedEndpoints };
+    });
   }
 
   handleRegionsChange(endpoint: Endpoint, newRegions: string[]) {
     this.setState(prevState => ({
-      multipleUploadedEndpoints: prevState.multipleUploadedEndpoints.map(stateEndpoint => {
-        if (typeof stateEndpoint !== 'string' && `${stateEndpoint.type}${stateEndpoint.name}` === `${endpoint.type}${endpoint.name}`) {
-          return {
-            ...stateEndpoint,
-            mapped_regions: newRegions,
+      multipleUploadedEndpoints: prevState.multipleUploadedEndpoints.map(
+        stateEndpoint => {
+          if (
+            typeof stateEndpoint !== "string" &&
+            `${stateEndpoint.type}${stateEndpoint.name}` ===
+              `${endpoint.type}${endpoint.name}`
+          ) {
+            return {
+              ...stateEndpoint,
+              mapped_regions: newRegions,
+            };
           }
+          return stateEndpoint;
         }
-        return stateEndpoint
-      }),
-    }))
+      ),
+    }));
   }
 
   renderMultipleUploadedEndpoints() {
     return (
       <MultipleUploadedEndpoints
         endpoints={this.state.multipleUploadedEndpoints}
-        onBackClick={() => { this.setState({ multipleUploadedEndpoints: [] }) }}
-        onRemove={(e, isAdded) => { this.handleRemoveUploadedEndpoint(e, isAdded) }}
+        onBackClick={() => {
+          this.setState({ multipleUploadedEndpoints: [] });
+        }}
+        onRemove={(e, isAdded) => {
+          this.handleRemoveUploadedEndpoint(e, isAdded);
+        }}
         validating={this.props.multiValidating}
         multiValidation={this.props.multiValidation}
         invalidRegionsEndpointIds={this.state.invalidRegionsEndpointIds}
         regions={this.props.regions}
-        onRegionsChange={(endpoint, newRegions) => { this.handleRegionsChange(endpoint, newRegions) }}
+        onRegionsChange={(endpoint, newRegions) => {
+          this.handleRegionsChange(endpoint, newRegions);
+        }}
         onValidateClick={() => {
-          this.props.onValidateMultipleEndpoints(this.state.multipleUploadedEndpoints.filter(e => typeof e !== 'string') as Endpoint[])
+          this.props.onValidateMultipleEndpoints(
+            this.state.multipleUploadedEndpoints.filter(
+              e => typeof e !== "string"
+            ) as Endpoint[]
+          );
         }}
         onDone={this.props.onCancelClick}
       />
-    )
+    );
   }
 
   renderLoading() {
     if (!this.props.loading) {
-      return null
+      return null;
     }
 
     return (
@@ -342,20 +397,23 @@ class ChooseProvider extends React.Component<Props, State> {
         <StatusImage loading />
         <LoadingText>Loading providers ...</LoadingText>
       </LoadingWrapper>
-    )
+    );
   }
 
   renderProviders() {
     if (this.props.loading) {
-      return null
+      return null;
     }
 
     const UploadButton = (
       <UploadMessageButton
-        onClick={() => { if (this.fileInput) this.fileInput.click() }}
-      >upload
+        onClick={() => {
+          if (this.fileInput) this.fileInput.click();
+        }}
+      >
+        upload
       </UploadMessageButton>
-    )
+    );
 
     return (
       <Providers>
@@ -365,39 +423,49 @@ class ChooseProvider extends React.Component<Props, State> {
               height={128}
               key={k}
               endpoint={k}
-              onClick={() => { this.props.onProviderClick(k) }}
+              onClick={() => {
+                this.props.onProviderClick(k);
+              }}
             />
           ))}
         </Logos>
         <Upload highlight={this.state.highlightDropzone}>
           <UploadMessage>
-            You can
-            &nbsp;{UploadButton}&nbsp;
-            or drop multiple .endpoint and zipped .endpoint files.
+            You can &nbsp;{UploadButton}&nbsp; or drop multiple .endpoint and
+            zipped .endpoint files.
           </UploadMessage>
         </Upload>
         <FakeFileInput
           type="file"
-          ref={r => { this.fileInput = r }}
+          ref={r => {
+            this.fileInput = r;
+          }}
           accept=".endpoint,.zip"
           multiple
-          onChange={e => { this.handleFileUpload(e.target.files) }}
+          onChange={e => {
+            this.handleFileUpload(e.target.files);
+          }}
         />
-        <Button secondary onClick={this.props.onCancelClick}>Cancel</Button>
+        <Button secondary onClick={this.props.onCancelClick}>
+          Cancel
+        </Button>
       </Providers>
-    )
+    );
   }
 
   render() {
     return (
       <Wrapper>
-        {this.state.multipleUploadedEndpoints.length === 0 ? this.renderProviders() : null}
+        {this.state.multipleUploadedEndpoints.length === 0
+          ? this.renderProviders()
+          : null}
         {this.renderLoading()}
         {this.state.multipleUploadedEndpoints.length > 0
-          ? this.renderMultipleUploadedEndpoints() : null}
+          ? this.renderMultipleUploadedEndpoints()
+          : null}
       </Wrapper>
-    )
+    );
   }
 }
 
-export default ChooseProvider
+export default ChooseProvider;

+ 108 - 105
src/components/modules/EndpointModule/ChooseProvider/MultipleUploadedEndpoints.tsx

@@ -12,45 +12,45 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
+import React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
 
-import type { Endpoint, MultiValidationItem } from '@src/@types/Endpoint'
+import type { Endpoint, MultiValidationItem } from "@src/@types/Endpoint";
 
-import StatusIcon from '@src/components/ui/StatusComponents/StatusIcon'
-import Button from '@src/components/ui/Button'
-import EndpointLogos from '@src/components/modules/EndpointModule/EndpointLogos'
-import LoadingButton from '@src/components/ui/LoadingButton'
+import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon";
+import Button from "@src/components/ui/Button";
+import EndpointLogos from "@src/components/modules/EndpointModule/EndpointLogos";
+import LoadingButton from "@src/components/ui/LoadingButton";
 
-import DomUtils from '@src/utils/DomUtils'
-import notificationStore from '@src/stores/NotificationStore'
-import DropdownLink from '@src/components/ui/Dropdowns/DropdownLink'
-import { Region } from '@src/@types/Region'
-import deleteHoverImage from './images/delete-hover.svg'
-import deleteImage from './images/delete.svg'
+import DomUtils from "@src/utils/DomUtils";
+import notificationStore from "@src/stores/NotificationStore";
+import DropdownLink from "@src/components/ui/Dropdowns/DropdownLink";
+import { Region } from "@src/@types/Region";
+import deleteHoverImage from "./images/delete-hover.svg";
+import deleteImage from "./images/delete.svg";
 
 const Wrapper = styled.div`
   width: 100%;
   min-height: 0;
-`
+`;
 const Buttons = styled.div`
   display: flex;
   justify-content: space-between;
   margin-top: 32px;
   flex-shrink: 0;
   padding: 0 32px;
-`
+`;
 const DeleteButton = styled.div`
   width: 16px;
   height: 16px;
-  background: url('${deleteImage}') center no-repeat;
+  background: url("${deleteImage}") center no-repeat;
   cursor: pointer;
 
   &:hover {
-    background: url('${deleteHoverImage}') center no-repeat;
+    background: url("${deleteHoverImage}") center no-repeat;
   }
-`
+`;
 const Content = styled.div`
   overflow: auto;
   display: flex;
@@ -59,141 +59,139 @@ const Content = styled.div`
   min-height: 200px;
   max-height: 384px;
   text-align: left;
-`
+`;
 const InvalidEndpoint = styled.div`
   margin-bottom: 8px;
-`
+`;
 const EndpointItem = styled.div`
   display: flex;
   align-items: center;
   margin-bottom: 8px;
-`
+`;
 const EndpointLogoWrapper = styled.div`
   min-width: 110px;
-`
+`;
 const EndpointData = styled.div`
   display: flex;
   align-items: center;
   justify-content: space-between;
   flex-grow: 1;
   overflow: hidden;
-`
+`;
 const EndpointName = styled.div`
   overflow: hidden;
   text-overflow: ellipsis;
-`
+`;
 const EndpointOptions = styled.div`
   display: flex;
   align-items: center;
-`
+`;
 const EndpointStatus = styled.div`
   display: flex;
   margin-right: 8px;
   > div {
-    margin-left:  8px;
+    margin-left: 8px;
   }
-`
+`;
 type Props = {
-  endpoints: (Endpoint | string)[],
-  regions: Region[],
-  invalidRegionsEndpointIds: { id: string, regions: string[] }[]
-  multiValidation: MultiValidationItem[],
-  validating: boolean,
-  onRegionsChange: (endpoint: Endpoint, newRegions: string[]) => void
-  onBackClick: () => void,
-  onRemove: (endpoint: Endpoint, isAdded: boolean) => void,
-  onValidateClick: () => void,
-  onDone: () => void,
-}
+  endpoints: (Endpoint | string)[];
+  regions: Region[];
+  invalidRegionsEndpointIds: { id: string; regions: string[] }[];
+  multiValidation: MultiValidationItem[];
+  validating: boolean;
+  onRegionsChange: (endpoint: Endpoint, newRegions: string[]) => void;
+  onBackClick: () => void;
+  onRemove: (endpoint: Endpoint, isAdded: boolean) => void;
+  onValidateClick: () => void;
+  onDone: () => void;
+};
 type State = {
-  validationDone: boolean,
-}
+  validationDone: boolean;
+};
 @observer
 class MultipleUploadedEndpoints extends React.Component<Props, State> {
   state = {
     validationDone: false,
-  }
+  };
 
   UNSAFE_componentWillReceiveProps(prevProps: Props) {
     if (prevProps.validating && !this.props.validating) {
-      this.setState({ validationDone: true })
+      this.setState({ validationDone: true });
     }
   }
 
   handleRemove(uploadedEndpoint: Endpoint) {
-    const multiEndpoint = this.props.multiValidation
-      .find(mv => mv.endpoint.name === uploadedEndpoint.name
-      && mv.endpoint.type === uploadedEndpoint.type)
+    const multiEndpoint = this.props.multiValidation.find(
+      mv =>
+        mv.endpoint.name === uploadedEndpoint.name &&
+        mv.endpoint.type === uploadedEndpoint.type
+    );
     if (multiEndpoint) {
-      this.props.onRemove(multiEndpoint.endpoint, true)
+      this.props.onRemove(multiEndpoint.endpoint, true);
     } else {
-      this.props.onRemove(uploadedEndpoint, false)
+      this.props.onRemove(uploadedEndpoint, false);
     }
   }
 
   copyErrorMessae(e: React.MouseEvent<HTMLDivElement>, message: string) {
-    if (e && e.stopPropagation) e.stopPropagation()
+    if (e && e.stopPropagation) e.stopPropagation();
 
-    const succesful = DomUtils.copyTextToClipboard(message)
+    const succesful = DomUtils.copyTextToClipboard(message);
 
     if (succesful) {
-      notificationStore.alert('The message has been copied to clipboard.')
+      notificationStore.alert("The message has been copied to clipboard.");
     } else {
-      notificationStore.alert('The message couldn\'t be copied', 'error')
+      notificationStore.alert("The message couldn't be copied", "error");
     }
   }
 
   renderButtons() {
-    let actionButton = null
+    let actionButton = null;
 
     if (this.props.validating) {
-      actionButton = <LoadingButton large>Validate and save</LoadingButton>
+      actionButton = <LoadingButton large>Validate and save</LoadingButton>;
     } else if (this.state.validationDone) {
       actionButton = (
-        <Button
-          large
-          primary
-          onClick={this.props.onDone}
-        >Done
+        <Button large primary onClick={this.props.onDone}>
+          Done
         </Button>
-      )
+      );
     } else {
       actionButton = (
-        <Button
-          large
-          primary
-          onClick={this.props.onValidateClick}
-        >Validate and save
+        <Button large primary onClick={this.props.onValidateClick}>
+          Validate and save
         </Button>
-      )
+      );
     }
 
     return (
       <Buttons>
-        <Button
-          large
-          secondary
-          onClick={this.props.onBackClick}
-        >Back
+        <Button large secondary onClick={this.props.onBackClick}>
+          Back
         </Button>
         {actionButton}
       </Buttons>
-    )
+    );
   }
 
   renderStatus(endpoint: Endpoint) {
-    const validationItem = this.props.multiValidation.find(v => v.endpoint.name === endpoint.name && v.endpoint.type === endpoint.type)
+    const validationItem = this.props.multiValidation.find(
+      v =>
+        v.endpoint.name === endpoint.name && v.endpoint.type === endpoint.type
+    );
     if (!validationItem) {
-      const invalidRegions = this.props.invalidRegionsEndpointIds.find(e => e.id === `${endpoint.type}${endpoint.name}`)?.regions
+      const invalidRegions = this.props.invalidRegionsEndpointIds.find(
+        e => e.id === `${endpoint.type}${endpoint.name}`
+      )?.regions;
       if (!invalidRegions?.length) {
-        return null
+        return null;
       }
       return (
         <>
           <DropdownLink
             width="200px"
             listWidth="120px"
-            getLabel={() => 'Coriolis Regions'}
+            getLabel={() => "Coriolis Regions"}
             multipleSelection
             selectedItems={endpoint.mapped_regions}
             items={this.props.regions.map(r => ({
@@ -202,79 +200,84 @@ class MultipleUploadedEndpoints extends React.Component<Props, State> {
             }))}
             onChange={item => {
               if (endpoint.mapped_regions.find(r => r === item.value)) {
-                this.props.onRegionsChange(endpoint, endpoint.mapped_regions.filter(r => r !== item.value))
+                this.props.onRegionsChange(
+                  endpoint,
+                  endpoint.mapped_regions.filter(r => r !== item.value)
+                );
               } else {
-                this.props.onRegionsChange(endpoint, [...endpoint.mapped_regions, item.value])
+                this.props.onRegionsChange(endpoint, [
+                  ...endpoint.mapped_regions,
+                  item.value,
+                ]);
               }
             }}
           />
           <StatusIcon
             status="INFO"
-            data-tip={`${invalidRegions.length} Coriolis Region${invalidRegions.length > 1 ? 's' : ''} couldn't be mapped for this endpoint. Use the Coriolis Regions dropdown to view and update the current mapping.`}
+            data-tip={`${invalidRegions.length} Coriolis Region${
+              invalidRegions.length > 1 ? "s" : ""
+            } couldn't be mapped for this endpoint. Use the Coriolis Regions dropdown to view and update the current mapping.`}
           />
         </>
-      )
+      );
     }
 
     if (validationItem.validating) {
-      return (
-        <StatusIcon status="RUNNING" />
-      )
+      return <StatusIcon status="RUNNING" />;
     }
-    const validation = validationItem.validation
+    const validation = validationItem.validation;
     if (validation) {
       if (validation.valid) {
-        return (
-          <StatusIcon status="COMPLETED" />
-        )
+        return <StatusIcon status="COMPLETED" />;
       }
       return (
         <StatusIcon
           status="WARNING"
-          onClick={e => { this.copyErrorMessae(e, validation.message) }}
+          onClick={e => {
+            this.copyErrorMessae(e, validation.message);
+          }}
           data-tip={validation.message}
-          style={{ cursor: 'pointer' }}
+          style={{ cursor: "pointer" }}
         />
-      )
+      );
     }
 
-    return null
+    return null;
   }
 
   renderContent() {
     return (
       <Content>
         {this.props.endpoints.map((endpoint, i) => {
-          if (typeof endpoint === 'string') {
+          if (typeof endpoint === "string") {
             return (
               // eslint-disable-next-line react/no-array-index-key
               <InvalidEndpoint key={i}>
                 File may contain an unsupported provider type: {endpoint}
               </InvalidEndpoint>
-            )
+            );
           }
           return (
             <EndpointItem key={`${endpoint.name}${String(endpoint.type)}`}>
               <EndpointLogoWrapper>
-                <EndpointLogos
-                  endpoint={endpoint.type}
-                  height={32}
-                />
+                <EndpointLogos endpoint={endpoint.type} height={32} />
               </EndpointLogoWrapper>
               <EndpointData>
                 <EndpointName>{endpoint.name}</EndpointName>
                 <EndpointOptions>
-                  <EndpointStatus>
-                    {this.renderStatus(endpoint)}
-                  </EndpointStatus>
-                  <DeleteButton onClick={() => { this.handleRemove(endpoint) }} />
+                  <EndpointStatus>{this.renderStatus(endpoint)}</EndpointStatus>
+                  <DeleteButton
+                    onClick={() => {
+                      this.handleRemove(endpoint);
+                    }}
+                  />
                 </EndpointOptions>
               </EndpointData>
             </EndpointItem>
-          )
+          );
         })}
       </Content>
-    )
+    );
   }
 
   render() {
@@ -283,8 +286,8 @@ class MultipleUploadedEndpoints extends React.Component<Props, State> {
         {this.renderContent()}
         {this.renderButtons()}
       </Wrapper>
-    )
+    );
   }
 }
 
-export default MultipleUploadedEndpoints
+export default MultipleUploadedEndpoints;

+ 1 - 2
src/components/modules/EndpointModule/ChooseProvider/package.json

@@ -2,6 +2,5 @@
   "name": "ChooseProvider",
   "version": "0.0.0",
   "private": true,
-  "main":"./ChooseProvider.tsx"
+  "main": "./ChooseProvider.tsx"
 }
-

+ 15 - 16
src/components/modules/EndpointModule/ChooseProvider/story.tsx

@@ -12,21 +12,20 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import ChooseProvider from '.'
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import ChooseProvider from ".";
 
 const providers: any = {
-  azure: { },
-  openstack: { },
-  opc: { },
-  oracle_vm: { },
-  vmware_vsphere: { },
-  aws: { },
-}
-const props: any = {}
-storiesOf('ChooseProvider', module)
-  .add('all', () => (
-    // eslint-disable-next-line react/jsx-props-no-spreading
-    <ChooseProvider {...props} providers={providers} />
-  ))
+  azure: {},
+  openstack: {},
+  opc: {},
+  oracle_vm: {},
+  vmware_vsphere: {},
+  aws: {},
+};
+const props: any = {};
+storiesOf("ChooseProvider", module).add("all", () => (
+  // eslint-disable-next-line react/jsx-props-no-spreading
+  <ChooseProvider {...props} providers={providers} />
+));

+ 40 - 38
src/components/modules/EndpointModule/ChooseProvider/test.tsx

@@ -12,42 +12,44 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TW from '@src/utils/TestWrapper'
-import ChooseProvider from '.'
-
-const wrap = props => new TW(shallow(
-  
-  <ChooseProvider {...props} />
-), 'cProvider')
-
-let providers = ['azure', 'openstack', 'opc', 'oracle_vm', 'vmware_vsphere', 'aws']
-
-describe('ChooseProvider Component', () => {
-  it('renders all given providers', () => {
-    let wrapper = wrap({ providers })
+import React from "react";
+import { shallow } from "enzyme";
+import sinon from "sinon";
+import TW from "@src/utils/TestWrapper";
+import ChooseProvider from ".";
+
+const wrap = props =>
+  new TW(shallow(<ChooseProvider {...props} />), "cProvider");
+
+const providers = [
+  "azure",
+  "openstack",
+  "opc",
+  "oracle_vm",
+  "vmware_vsphere",
+  "aws",
+];
+
+describe("ChooseProvider Component", () => {
+  it("renders all given providers", () => {
+    const wrapper = wrap({ providers });
     providers.forEach(key => {
-      expect(wrapper.find(`endpointLogo-${key}`).prop('endpoint')).toBe(key)
-    })
-  })
-
-  it('dispatches provider click', () => {
-    let onProviderClick = sinon.spy()
-    let wrapper = wrap({ providers, onProviderClick })
-    wrapper.find('endpointLogo-opc').click()
-    expect(onProviderClick.calledOnce).toBe(true)
-    expect(onProviderClick.args[0][0]).toBe('opc')
-  })
-
-  it('dispatches cancel click', () => {
-    let onCancelClick = sinon.spy()
-    let wrapper = wrap({ providers, onCancelClick })
-    wrapper.find('cancelButton').click()
-    expect(onCancelClick.calledOnce).toBe(true)
-  })
-})
-
-
-
+      expect(wrapper.find(`endpointLogo-${key}`).prop("endpoint")).toBe(key);
+    });
+  });
+
+  it("dispatches provider click", () => {
+    const onProviderClick = sinon.spy();
+    const wrapper = wrap({ providers, onProviderClick });
+    wrapper.find("endpointLogo-opc").click();
+    expect(onProviderClick.calledOnce).toBe(true);
+    expect(onProviderClick.args[0][0]).toBe("opc");
+  });
+
+  it("dispatches cancel click", () => {
+    const onCancelClick = sinon.spy();
+    const wrapper = wrap({ providers, onCancelClick });
+    wrapper.find("cancelButton").click();
+    expect(onCancelClick.calledOnce).toBe(true);
+  });
+});

+ 129 - 97
src/components/modules/EndpointModule/EndpointDetailsContent/EndpointDetailsContent.tsx

@@ -12,79 +12,82 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import * as React from 'react'
-import { Link } from 'react-router-dom'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
+import * as React from "react";
+import { Link } from "react-router-dom";
+import { observer } from "mobx-react";
+import styled from "styled-components";
 
-import EndpointLogos from '@src/components/modules/EndpointModule/EndpointLogos'
-import PasswordValue from '@src/components/ui/PasswordValue'
-import Button from '@src/components/ui/Button'
-import CopyValue from '@src/components/ui/CopyValue'
-import CopyMultilineValue from '@src/components/ui/CopyMultilineValue'
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
+import EndpointLogos from "@src/components/modules/EndpointModule/EndpointLogos";
+import PasswordValue from "@src/components/ui/PasswordValue";
+import Button from "@src/components/ui/Button";
+import CopyValue from "@src/components/ui/CopyValue";
+import CopyMultilineValue from "@src/components/ui/CopyMultilineValue";
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
 
-import type { Endpoint } from '@src/@types/Endpoint'
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import DateUtils from '@src/utils/DateUtils'
-import LabelDictionary from '@src/utils/LabelDictionary'
-import configLoader from '@src/utils/Config'
-import { Region } from '@src/@types/Region'
+import type { Endpoint } from "@src/@types/Endpoint";
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
+import DateUtils from "@src/utils/DateUtils";
+import LabelDictionary from "@src/utils/LabelDictionary";
+import configLoader from "@src/utils/Config";
+import { Region } from "@src/@types/Region";
 import {
-  getTransferItemTitle, MigrationItem, ReplicaItem, TransferItem,
-} from '@src/@types/MainItem'
-import { Field as FieldType } from '@src/@types/Field'
-import DomUtils from '@src/utils/DomUtils'
+  getTransferItemTitle,
+  MigrationItem,
+  ReplicaItem,
+  TransferItem,
+} from "@src/@types/MainItem";
+import { Field as FieldType } from "@src/@types/Field";
+import DomUtils from "@src/utils/DomUtils";
 
 const Wrapper = styled.div<any>`
   ${ThemeProps.exactWidth(ThemeProps.contentWidth)}
   margin: 0 auto;
   padding-left: 126px;
-`
+`;
 const Info = styled.div<any>`
   display: flex;
   flex-wrap: wrap;
   margin-top: 32px;
   margin-left: -32px;
-`
+`;
 const Field = styled.div<any>`
-  ${ThemeProps.exactWidth('calc(50% - 32px)')}
+  ${ThemeProps.exactWidth("calc(50% - 32px)")}
   margin-bottom: 32px;
   margin-left: 32px;
-`
+`;
 const Label = styled.div<any>`
   font-size: 10px;
   font-weight: ${ThemeProps.fontWeights.medium};
   color: ${ThemePalette.grayscale[3]};
   text-transform: uppercase;
   margin-bottom: 3px;
-`
-const Value = styled.div<any>``
+`;
+const Value = styled.div<any>``;
 const Buttons = styled.div<any>`
   display: flex;
   justify-content: space-between;
   margin-top: 64px;
-`
-const MainButtons = styled.div<any>``
-const DeleteButton = styled.div<any>``
+`;
+const MainButtons = styled.div<any>``;
+const DeleteButton = styled.div<any>``;
 const LoadingWrapper = styled.div<any>`
   display: flex;
   justify-content: center;
   width: 100%;
   margin: 32px 0 64px 0;
-`
+`;
 const LinkStyled = styled(Link)`
   color: ${ThemePalette.primary};
   text-decoration: none;
   cursor: pointer;
-`
+`;
 const TransferItems = styled.div`
   max-height: 200px;
   overflow: auto;
-`
+`;
 const TransferItemWrapper = styled.div`
   margin-bottom: 4px;
-`
+`;
 
 const DownloadLink = styled.div`
   display: inline-block;
@@ -93,87 +96,94 @@ const DownloadLink = styled.div`
   :hover {
     text-decoration: underline;
   }
-`
+`;
 
 type Props = {
-  item: Endpoint | null,
-  regions: Region[],
-  connectionInfo: Endpoint['connection_info'] | null,
-  loading: boolean,
-  usage: { migrations: MigrationItem[], replicas: ReplicaItem[] },
-  connectionInfoSchema: FieldType[],
-  onDeleteClick: () => void,
-  onValidateClick: () => void,
-}
+  item: Endpoint | null;
+  regions: Region[];
+  connectionInfo: Endpoint["connection_info"] | null;
+  loading: boolean;
+  usage: { migrations: MigrationItem[]; replicas: ReplicaItem[] };
+  connectionInfoSchema: FieldType[];
+  onDeleteClick: () => void;
+  onValidateClick: () => void;
+};
 @observer
 class EndpointDetailsContent extends React.Component<Props> {
-  renderedKeys!: { [prop: string]: boolean }
+  renderedKeys!: { [prop: string]: boolean };
 
   renderDownloadValue(value: string, fieldName: string) {
-    const endpoint = this.props.item
+    const endpoint = this.props.item;
     if (!endpoint) {
-      return null
+      return null;
     }
     return (
-      <DownloadLink onClick={() => {
-        DomUtils.download(value, fieldName)
-      }}
-      >Download
+      <DownloadLink
+        onClick={() => {
+          DomUtils.download(value, fieldName);
+        }}
+      >
+        Download
       </DownloadLink>
-    )
+    );
   }
 
   renderConnectionInfoLoading() {
     if (!this.props.loading) {
-      return null
+      return null;
     }
 
     return (
       <LoadingWrapper>
         <StatusImage loading />
       </LoadingWrapper>
-    )
+    );
   }
 
   renderConnectionInfo(connectionInfo: any): React.ReactNode {
     if (!connectionInfo) {
-      return null
+      return null;
     }
 
     return Object.keys(connectionInfo).map(key => {
-      let value = connectionInfo[key]
+      let value = connectionInfo[key];
 
-      if (key === 'secret_ref') {
-        return null
+      if (key === "secret_ref") {
+        return null;
       }
 
-      if (typeof connectionInfo[key] === 'object') {
-        return this.renderConnectionInfo(connectionInfo[key])
+      if (typeof connectionInfo[key] === "object") {
+        return this.renderConnectionInfo(connectionInfo[key]);
       }
 
       if (this.renderedKeys[key]) {
-        return null
+        return null;
       }
 
-      this.renderedKeys[key] = true
+      this.renderedKeys[key] = true;
 
       if (value === true) {
-        value = 'Yes'
+        value = "Yes";
       } else if (value === false) {
-        value = 'No'
+        value = "No";
       } else if (!value) {
-        value = '-'
+        value = "-";
       }
 
-      let valueElement = null
-      const schemaField = this.props.connectionInfoSchema.find(f => f.name === key)
+      let valueElement = null;
+      const schemaField = this.props.connectionInfoSchema.find(
+        f => f.name === key
+      );
 
-      if (configLoader.config.passwordFields.find(fn => fn === key) || key.indexOf('password') > -1) {
-        valueElement = <PasswordValue value={value} />
+      if (
+        configLoader.config.passwordFields.find(fn => fn === key) ||
+        key.indexOf("password") > -1
+      ) {
+        valueElement = <PasswordValue value={value} />;
       } else if (schemaField?.useFile) {
-        valueElement = this.renderDownloadValue(value, key)
+        valueElement = this.renderDownloadValue(value, key);
       } else {
-        valueElement = this.renderValue(value)
+        valueElement = this.renderValue(value);
       }
 
       return (
@@ -181,60 +191,70 @@ class EndpointDetailsContent extends React.Component<Props> {
           <Label>{LabelDictionary.get(key)}</Label>
           {valueElement}
         </Field>
-      )
-    })
+      );
+    });
   }
 
   renderButtons() {
     return (
       <Buttons>
         <MainButtons>
-          <Button onClick={this.props.onValidateClick}>Validate Endpoint</Button>
+          <Button onClick={this.props.onValidateClick}>
+            Validate Endpoint
+          </Button>
         </MainButtons>
         <DeleteButton>
-          <Button hollow alert onClick={this.props.onDeleteClick}>Delete Endpoint</Button>
+          <Button hollow alert onClick={this.props.onDeleteClick}>
+            Delete Endpoint
+          </Button>
         </DeleteButton>
       </Buttons>
-    )
+    );
   }
 
   renderValue(value: string) {
-    return <CopyValue value={value} maxWidth="90%" />
+    return <CopyValue value={value} maxWidth="90%" />;
   }
 
   renderRegions() {
     return (
       <span>
-        {this.props.item?.mapped_regions.map(regionId => this.props.regions.find(r => r.id === regionId)?.name).join(', ') || '-'}
+        {this.props.item?.mapped_regions
+          .map(
+            regionId => this.props.regions.find(r => r.id === regionId)?.name
+          )
+          .join(", ") || "-"}
       </span>
-    )
+    );
   }
 
   renderUsage(items: TransferItem[]) {
     return (
       <TransferItems>
         {items.map(item => (
-          <TransferItemWrapper>
-            <LinkStyled
-              key={item.id}
-              to={`/${item.type}s/${item.id}`}
-            >
+          <TransferItemWrapper key={item.id}>
+            <LinkStyled to={`/${item.type}s/${item.id}`}>
               {getTransferItemTitle(item)}
             </LinkStyled>
           </TransferItemWrapper>
         ))}
       </TransferItems>
-    )
+    );
   }
 
   render() {
-    this.renderedKeys = {}
+    this.renderedKeys = {};
     const {
       // eslint-disable-next-line @typescript-eslint/naming-convention
-      type, name, description, created_at, id,
-    } = this.props.item || {}
-    const usage: TransferItem[] = this.props.usage.replicas
-      .concat(this.props.usage.migrations as any[])
+      type,
+      name,
+      description,
+      created_at,
+      id,
+    } = this.props.item || {};
+    const usage: TransferItem[] = this.props.usage.replicas.concat(
+      this.props.usage.migrations as any[]
+    );
 
     return (
       <Wrapper>
@@ -242,15 +262,19 @@ class EndpointDetailsContent extends React.Component<Props> {
         <Info>
           <Field>
             <Label>Id</Label>
-            {this.renderValue(id || '')}
+            {this.renderValue(id || "")}
           </Field>
           <Field>
             <Label>Name</Label>
-            {this.renderValue(name || '')}
+            {this.renderValue(name || "")}
           </Field>
           <Field>
             <Label>Type</Label>
-            {this.renderValue(this.props.item ? configLoader.config.providerNames[this.props.item.type] : '')}
+            {this.renderValue(
+              this.props.item
+                ? configLoader.config.providerNames[this.props.item.type]
+                : ""
+            )}
           </Field>
           <Field>
             <Label>Coriolis Regions</Label>
@@ -258,23 +282,31 @@ class EndpointDetailsContent extends React.Component<Props> {
           </Field>
           <Field>
             <Label>Description</Label>
-            {description ? <CopyMultilineValue value={description} /> : <Value>-</Value>}
+            {description ? (
+              <CopyMultilineValue value={description} />
+            ) : (
+              <Value>-</Value>
+            )}
           </Field>
           <Field>
             <Label>Created</Label>
-            {this.renderValue(DateUtils.getLocalTime(created_at).format('DD/MM/YYYY HH:mm'))}
+            {this.renderValue(
+              DateUtils.getLocalTime(created_at).format("DD/MM/YYYY HH:mm")
+            )}
           </Field>
           <Field>
             <Label>Used in replicas/migrations ({usage.length})</Label>
             {usage.length > 0 ? this.renderUsage(usage) : <Value>-</Value>}
           </Field>
-          {!this.props.connectionInfo ? this.renderConnectionInfoLoading() : null}
+          {!this.props.connectionInfo
+            ? this.renderConnectionInfoLoading()
+            : null}
           {this.renderConnectionInfo(this.props.connectionInfo)}
         </Info>
         {this.renderButtons()}
       </Wrapper>
-    )
+    );
   }
 }
 
-export default EndpointDetailsContent
+export default EndpointDetailsContent;

+ 1 - 2
src/components/modules/EndpointModule/EndpointDetailsContent/package.json

@@ -2,6 +2,5 @@
   "name": "EndpointDetailsContent",
   "version": "0.0.0",
   "private": true,
-  "main":"./EndpointDetailsContent.tsx"
+  "main": "./EndpointDetailsContent.tsx"
 }
-

+ 21 - 17
src/components/modules/EndpointModule/EndpointDetailsContent/story.tsx

@@ -13,27 +13,31 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import EndpointDetailsContent from '.'
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import EndpointDetailsContent from ".";
 
 const item: any = {
-  name: 'Name',
-  type: 'openstack',
-  description: 'Description',
+  name: "Name",
+  type: "openstack",
+  description: "Description",
   created_at: new Date(),
-}
+};
 
 const connectionInfo: any = {
-  username: 'username',
-  password: 'password123',
-  details: 'other details',
-}
-const props: any = {}
-storiesOf('EndpointDetailsContent', module)
-  .add('connection info loading', () => (
+  username: "username",
+  password: "password123",
+  details: "other details",
+};
+const props: any = {};
+storiesOf("EndpointDetailsContent", module)
+  .add("connection info loading", () => (
     <EndpointDetailsContent item={item} loading {...props} />
   ))
-  .add('with connection info', () => (
-    <EndpointDetailsContent item={item} connectionInfo={connectionInfo} {...props} />
-  ))
+  .add("with connection info", () => (
+    <EndpointDetailsContent
+      item={item}
+      connectionInfo={connectionInfo}
+      {...props}
+    />
+  ));

+ 98 - 79
src/components/modules/EndpointModule/EndpointDetailsContent/test.tsx

@@ -12,90 +12,109 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import moment from 'moment'
-import TW from '@src/utils/TestWrapper'
-import EndpointDetailsContent from '.'
-
-import configLoader from '@src/utils/Config'
-
-const wrap = props => new TW(shallow(
-
-  <EndpointDetailsContent usage={{ replicas: [], migrations: [] }}{...props} />
-), 'edContent')
-
-let item = {
-  name: 'endpoint_name',
-  type: 'openstack',
-  description: 'endpoint_description',
+import React from "react";
+import { shallow } from "enzyme";
+import sinon from "sinon";
+import moment from "moment";
+import TW from "@src/utils/TestWrapper";
+import EndpointDetailsContent from ".";
+
+import configLoader from "@src/utils/Config";
+
+const wrap = props =>
+  new TW(
+    shallow(
+      <EndpointDetailsContent
+        usage={{ replicas: [], migrations: [] }}
+        {...props}
+      />
+    ),
+    "edContent"
+  );
+
+const item = {
+  name: "endpoint_name",
+  type: "openstack",
+  description: "endpoint_description",
   created_at: new Date(2017, 10, 24, 13, 56),
-}
+};
 
-let connectionInfo = {
-  username: 'username',
-  password: 'password123',
-  details: 'other details',
+const connectionInfo = {
+  username: "username",
+  password: "password123",
+  details: "other details",
   boolean_true: true,
   boolean_false: false,
   nested: {
-    nested_1: 'nested_first',
-    nested_2: 'nested_second',
+    nested_1: "nested_first",
+    nested_2: "nested_second",
   },
-}
+};
 
-describe('EndpointDetailsContent Component', () => {
+describe("EndpointDetailsContent Component", () => {
   beforeAll(() => {
-
-    configLoader.config = { passwordFields: [] }
-  })
-
-  it('renders endpoint details', () => {
-    let wrapper = wrap({ item })
-    expect(wrapper.find('name').prop('value')).toBe(item.name)
-    expect(wrapper.find('description').prop('value')).toBe(item.description)
-    expect(wrapper.find('created').prop('value'))
-      .toBe(moment(item.created_at).add(-new Date().getTimezoneOffset(), 'minutes').format('DD/MM/YYYY HH:mm'))
-    expect(wrapper.find('connLoading').length).toBe(0)
-  })
-
-  it('renders connection info loading', () => {
-    let wrapper = wrap({ item, loading: true })
-    expect(wrapper.find('name').prop('value')).toBe(item.name)
-    expect(wrapper.find('connLoading').length).toBe(1)
-  })
-
-  it('renders simple connection info', () => {
-    let wrapper = wrap({ item, connectionInfo, passwordFields: ['password'] })
-    expect(wrapper.find('connValue-username').prop('value')).toBe(connectionInfo.username)
-    expect(wrapper.find('connPassword').prop('value')).toBe(connectionInfo.password)
-    expect(wrapper.find('connValue-details').prop('value')).toBe(connectionInfo.details)
-  })
-
-  it('renders boolean as Yes and No', () => {
-    let wrapper = wrap({ item, connectionInfo })
-    expect(wrapper.find('connValue-boolean_true').prop('value')).toBe('Yes')
-    expect(wrapper.find('connValue-boolean_false').prop('value')).toBe('No')
-  })
-
-  it('renders nested connection info', () => {
-    let wrapper = wrap({ item, connectionInfo })
-    expect(wrapper.find('connValue-nested_1').prop('value')).toBe(connectionInfo.nested.nested_1)
-    expect(wrapper.find('connValue-nested_2').prop('value')).toBe(connectionInfo.nested.nested_2)
-  })
-
-  it('dispatches buttons clicks', () => {
-    let onDeleteClick = sinon.spy()
-    let onValidateClick = sinon.spy()
-
-    let wrapper = wrap({ item, onDeleteClick, onValidateClick })
-    wrapper.find('validateButton').click()
-    wrapper.find('deleteButton').click()
-    expect(onValidateClick.calledOnce).toBe(true)
-    expect(onDeleteClick.calledOnce).toBe(true)
-  })
-})
-
-
-
+    configLoader.config = { passwordFields: [] };
+  });
+
+  it("renders endpoint details", () => {
+    const wrapper = wrap({ item });
+    expect(wrapper.find("name").prop("value")).toBe(item.name);
+    expect(wrapper.find("description").prop("value")).toBe(item.description);
+    expect(wrapper.find("created").prop("value")).toBe(
+      moment(item.created_at)
+        .add(-new Date().getTimezoneOffset(), "minutes")
+        .format("DD/MM/YYYY HH:mm")
+    );
+    expect(wrapper.find("connLoading").length).toBe(0);
+  });
+
+  it("renders connection info loading", () => {
+    const wrapper = wrap({ item, loading: true });
+    expect(wrapper.find("name").prop("value")).toBe(item.name);
+    expect(wrapper.find("connLoading").length).toBe(1);
+  });
+
+  it("renders simple connection info", () => {
+    const wrapper = wrap({
+      item,
+      connectionInfo,
+      passwordFields: ["password"],
+    });
+    expect(wrapper.find("connValue-username").prop("value")).toBe(
+      connectionInfo.username
+    );
+    expect(wrapper.find("connPassword").prop("value")).toBe(
+      connectionInfo.password
+    );
+    expect(wrapper.find("connValue-details").prop("value")).toBe(
+      connectionInfo.details
+    );
+  });
+
+  it("renders boolean as Yes and No", () => {
+    const wrapper = wrap({ item, connectionInfo });
+    expect(wrapper.find("connValue-boolean_true").prop("value")).toBe("Yes");
+    expect(wrapper.find("connValue-boolean_false").prop("value")).toBe("No");
+  });
+
+  it("renders nested connection info", () => {
+    const wrapper = wrap({ item, connectionInfo });
+    expect(wrapper.find("connValue-nested_1").prop("value")).toBe(
+      connectionInfo.nested.nested_1
+    );
+    expect(wrapper.find("connValue-nested_2").prop("value")).toBe(
+      connectionInfo.nested.nested_2
+    );
+  });
+
+  it("dispatches buttons clicks", () => {
+    const onDeleteClick = sinon.spy();
+    const onValidateClick = sinon.spy();
+
+    const wrapper = wrap({ item, onDeleteClick, onValidateClick });
+    wrapper.find("validateButton").click();
+    wrapper.find("deleteButton").click();
+    expect(onValidateClick.calledOnce).toBe(true);
+    expect(onDeleteClick.calledOnce).toBe(true);
+  });
+});

+ 56 - 43
src/components/modules/EndpointModule/EndpointDuplicateOptions/EndpointDuplicateOptions.tsx

@@ -12,92 +12,96 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { observer } from 'mobx-react'
-import styled from 'styled-components'
+import React from "react";
+import { observer } from "mobx-react";
+import styled from "styled-components";
 
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
-import Button from '@src/components/ui/Button'
-import FieldInput from '@src/components/ui/FieldInput'
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
+import Button from "@src/components/ui/Button";
+import FieldInput from "@src/components/ui/FieldInput";
 
-import KeyboardManager from '@src/utils/KeyboardManager'
-import type { Project } from '@src/@types/Project'
-import { ThemePalette } from '@src/components/Theme'
+import KeyboardManager from "@src/utils/KeyboardManager";
+import type { Project } from "@src/@types/Project";
+import { ThemePalette } from "@src/components/Theme";
 
-import duplicateImage from './images/duplicate.svg'
+import duplicateImage from "./images/duplicate.svg";
 
 const Wrapper = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
   padding: 0 32px 32px 32px;
-`
+`;
 const Options = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
   width: 100%;
-`
+`;
 const Image = styled.div<any>`
   margin-top: 48px;
   margin-bottom: 80px;
   width: 128px;
   height: 96px;
-  background: url('${duplicateImage}') no-repeat center;
-`
+  background: url("${duplicateImage}") no-repeat center;
+`;
 const Message = styled.div<any>`
   margin-top: 48px;
   text-align: center;
-`
+`;
 const Title = styled.div<any>`
   font-size: 18px;
   margin-bottom: 8px;
-`
+`;
 const Subtitle = styled.div<any>`
   color: ${ThemePalette.grayscale[4]};
-`
+`;
 const Form = styled.div<any>`
   margin-bottom: 128px;
-`
+`;
 const Loading = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
   margin-top: 32px;
-`
+`;
 const Buttons = styled.div<any>`
   display: flex;
   justify-content: space-between;
   width: 100%;
-`
+`;
 const FieldInputStyled = styled(FieldInput)`
   width: 319px;
   justify-content: space-between;
-`
+`;
 type Props = {
-  projects: Project[],
-  selectedProjectId: string,
-  duplicating: boolean,
-  onCancelClick: () => void,
-  onDuplicateClick: (projectId: string) => void,
-}
+  projects: Project[];
+  selectedProjectId: string;
+  duplicating: boolean;
+  onCancelClick: () => void;
+  onDuplicateClick: (projectId: string) => void;
+};
 type State = {
-  selectedProjectId: string,
-}
+  selectedProjectId: string;
+};
 @observer
 class EndpointDuplicateOptions extends React.Component<Props, State> {
   UNSAFE_componentWillMount() {
-    this.setState({ selectedProjectId: this.props.selectedProjectId })
+    this.setState({ selectedProjectId: this.props.selectedProjectId });
   }
 
   componentDidMount() {
-    KeyboardManager.onEnter('duplicate-options', () => {
-      this.props.onDuplicateClick(this.state.selectedProjectId)
-    }, 2)
+    KeyboardManager.onEnter(
+      "duplicate-options",
+      () => {
+        this.props.onDuplicateClick(this.state.selectedProjectId);
+      },
+      2
+    );
   }
 
   componentWillUnmount() {
-    KeyboardManager.removeKeyDown('duplicate-options')
+    KeyboardManager.removeKeyDown("duplicate-options");
   }
 
   renderDuplicating() {
@@ -109,7 +113,7 @@ class EndpointDuplicateOptions extends React.Component<Props, State> {
           <Subtitle>Please wait ...</Subtitle>
         </Message>
       </Loading>
-    )
+    );
   }
 
   renderOptions() {
@@ -123,29 +127,38 @@ class EndpointDuplicateOptions extends React.Component<Props, State> {
             type="string"
             enum={this.props.projects}
             value={this.state.selectedProjectId}
-            onChange={projectId => { this.setState({ selectedProjectId: projectId }) }}
+            onChange={projectId => {
+              this.setState({ selectedProjectId: projectId });
+            }}
             width={318}
             layout="page"
           />
         </Form>
         <Buttons>
-          <Button secondary onClick={this.props.onCancelClick}>Cancel</Button>
+          <Button secondary onClick={this.props.onCancelClick}>
+            Cancel
+          </Button>
           <Button
-            onClick={() => { this.props.onDuplicateClick(this.state.selectedProjectId) }}
-          >Duplicate
+            onClick={() => {
+              this.props.onDuplicateClick(this.state.selectedProjectId);
+            }}
+          >
+            Duplicate
           </Button>
         </Buttons>
       </Options>
-    )
+    );
   }
 
   render() {
     return (
       <Wrapper>
-        {this.props.duplicating ? this.renderDuplicating() : this.renderOptions()}
+        {this.props.duplicating
+          ? this.renderDuplicating()
+          : this.renderOptions()}
       </Wrapper>
-    )
+    );
   }
 }
 
-export default EndpointDuplicateOptions
+export default EndpointDuplicateOptions;

+ 1 - 2
src/components/modules/EndpointModule/EndpointDuplicateOptions/package.json

@@ -2,6 +2,5 @@
   "name": "EndpointDuplicateOptions",
   "version": "0.0.0",
   "private": true,
-  "main":"./EndpointDuplicateOptions.tsx"
+  "main": "./EndpointDuplicateOptions.tsx"
 }
-

+ 15 - 15
src/components/modules/EndpointModule/EndpointDuplicateOptions/story.tsx

@@ -12,30 +12,30 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import Component from '.'
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import Component from ".";
 
-storiesOf('EndpointDuplicateOptions', module)
-  .add('default', () => (
-    <div style={{ width: '576px', background: 'white' }}>
+storiesOf("EndpointDuplicateOptions", module)
+  .add("default", () => (
+    <div style={{ width: "576px", background: "white" }}>
       <Component
         duplicating={false}
-        onCancelClick={() => { }}
-        onDuplicateClick={() => { }}
-        projects={[{ name: 'admin', id: 'admin' }]}
+        onCancelClick={() => {}}
+        onDuplicateClick={() => {}}
+        projects={[{ name: "admin", id: "admin" }]}
         selectedProjectId="admin"
       />
     </div>
   ))
-  .add('duplicating', () => (
-    <div style={{ width: '576px' }}>
+  .add("duplicating", () => (
+    <div style={{ width: "576px" }}>
       <Component
         duplicating
-        onCancelClick={() => { }}
-        onDuplicateClick={() => { }}
-        projects={[{ name: 'admin', id: 'admin' }]}
+        onCancelClick={() => {}}
+        onDuplicateClick={() => {}}
+        projects={[{ name: "admin", id: "admin" }]}
         selectedProjectId="admin"
       />
     </div>
-  ))
+  ));

+ 48 - 48
src/components/modules/EndpointModule/EndpointDuplicateOptions/test.tsx

@@ -12,64 +12,64 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TW from '@src/utils/TestWrapper'
-import type { Project } from '@src/@types/Project'
-import EndpointDuplicateOptions from '.'
+import React from "react";
+import { shallow } from "enzyme";
+import sinon from "sinon";
+import TW from "@src/utils/TestWrapper";
+import type { Project } from "@src/@types/Project";
+import EndpointDuplicateOptions from ".";
 
 type Props = {
-  projects: Project[],
-  selectedProjectId: string,
-  duplicating: boolean,
-  onCancelClick: () => void,
-  onDuplicateClick: (projectId: string) => void,
-}
+  projects: Project[];
+  selectedProjectId: string;
+  duplicating: boolean;
+  onCancelClick: () => void;
+  onDuplicateClick: (projectId: string) => void;
+};
 
-const wrap = (props: Props) => new TW(shallow(<EndpointDuplicateOptions {...props} />), 'edOptions')
+const wrap = (props: Props) =>
+  new TW(shallow(<EndpointDuplicateOptions {...props} />), "edOptions");
 const projects: Project[] = [
-  { id: 'project-1', name: 'Project 1' },
-  { id: 'project-2', name: 'Project 2' },
-]
-describe('EndpointDuplicateOptions Component', () => {
-  it('renders projects', () => {
-    let wrapper = wrap({
+  { id: "project-1", name: "Project 1" },
+  { id: "project-2", name: "Project 2" },
+];
+describe("EndpointDuplicateOptions Component", () => {
+  it("renders projects", () => {
+    const wrapper = wrap({
       projects,
-      selectedProjectId: 'project-2',
+      selectedProjectId: "project-2",
       duplicating: false,
-      onCancelClick: () => { },
-      onDuplicateClick: () => { },
-    })
-    expect(wrapper.find('field-project').prop('enum')[1].name).toBe(projects[1].name)
-    expect(wrapper.find('field-project').prop('value')).toBe('project-2')
-    expect(wrapper.find('loading').length).toBe(0)
-  })
+      onCancelClick: () => {},
+      onDuplicateClick: () => {},
+    });
+    expect(wrapper.find("field-project").prop("enum")[1].name).toBe(
+      projects[1].name
+    );
+    expect(wrapper.find("field-project").prop("value")).toBe("project-2");
+    expect(wrapper.find("loading").length).toBe(0);
+  });
 
-  it('dispatches duplicate', () => {
-    let onDuplicateClick = sinon.spy()
-    let wrapper = wrap({
+  it("dispatches duplicate", () => {
+    const onDuplicateClick = sinon.spy();
+    const wrapper = wrap({
       projects,
-      selectedProjectId: 'project-2',
+      selectedProjectId: "project-2",
       duplicating: false,
-      onCancelClick: () => { },
+      onCancelClick: () => {},
       onDuplicateClick,
-    })
-    wrapper.find('duplicateButton').click()
-    expect(onDuplicateClick.args[0][0]).toBe('project-2')
-  })
+    });
+    wrapper.find("duplicateButton").click();
+    expect(onDuplicateClick.args[0][0]).toBe("project-2");
+  });
 
-  it('renders loading', () => {
-    let wrapper = wrap({
+  it("renders loading", () => {
+    const wrapper = wrap({
       projects,
-      selectedProjectId: 'project-2',
+      selectedProjectId: "project-2",
       duplicating: true,
-      onCancelClick: () => { },
-      onDuplicateClick: () => { },
-    })
-    expect(wrapper.find('loading').length).toBe(1)
-  })
-})
-
-
-
+      onCancelClick: () => {},
+      onDuplicateClick: () => {},
+    });
+    expect(wrapper.find("loading").length).toBe(1);
+  });
+});

+ 38 - 32
src/components/modules/EndpointModule/EndpointListItem/EndpointListItem.tsx

@@ -12,22 +12,22 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import styled from 'styled-components'
-import { observer } from 'mobx-react'
+import React from "react";
+import styled from "styled-components";
+import { observer } from "mobx-react";
 
-import type { Endpoint } from '@src/@types/Endpoint'
-import Checkbox from '@src/components/ui/Checkbox'
-import EndpointLogos from '@src/components/modules/EndpointModule/EndpointLogos'
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import DateUtils from '@src/utils/DateUtils'
+import type { Endpoint } from "@src/@types/Endpoint";
+import Checkbox from "@src/components/ui/Checkbox";
+import EndpointLogos from "@src/components/modules/EndpointModule/EndpointLogos";
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
+import DateUtils from "@src/utils/DateUtils";
 
-import endpointImage from './images/endpoint.svg'
+import endpointImage from "./images/endpoint.svg";
 
 const CheckboxStyled = styled(Checkbox)`
   opacity: ${props => (props.checked ? 1 : 0)};
   transition: all ${ThemeProps.animations.swift};
-`
+`;
 const Content = styled.div<any>`
   display: flex;
   align-items: center;
@@ -42,7 +42,7 @@ const Content = styled.div<any>`
   &:hover {
     background: ${ThemePalette.grayscale[1]};
   }
-`
+`;
 const Wrapper = styled.div<any>`
   display: flex;
   align-items: center;
@@ -54,49 +54,52 @@ const Wrapper = styled.div<any>`
   &:last-child ${Content} {
     border-bottom: 1px solid ${ThemePalette.grayscale[1]};
   }
-`
+`;
 const Image = styled.div<any>`
   min-width: 48px;
   height: 48px;
-  background: url('${props => props.image}') no-repeat center;
+  background: url("${props => props.image}") no-repeat center;
   margin-right: 16px;
-`
+`;
 const Title = styled.div<any>`
   flex-grow: 1;
   overflow: hidden;
   margin-right: 48px;
   min-width: 100px;
-`
+`;
 const TitleLabel = styled.div<any>`
   font-size: 16px;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-`
+`;
 const Subtitle = styled.div<any>`
   color: ${ThemePalette.grayscale[4]};
   margin-top: 3px;
-`
+`;
 const ItemLabel = styled.div<any>`
   color: ${ThemePalette.grayscale[4]};
-`
+`;
 const ItemValue = styled.div<any>`
   color: ${ThemePalette.primary};
-`
+`;
 const Created = styled.div<any>`
   margin: 0 48px;
   min-width: 175px;
-`
+`;
 const Usage = styled.div<any>`
   min-width: 244px;
-`
+`;
 type Props = {
-  item: Endpoint,
-  onClick: () => void,
-  selected: boolean,
-  onSelectedChange: (value: boolean) => void,
-  getUsage: (item: Endpoint) => { replicasCount: number, migrationsCount: number },
-}
+  item: Endpoint;
+  onClick: () => void;
+  selected: boolean;
+  onSelectedChange: (value: boolean) => void;
+  getUsage: (item: Endpoint) => {
+    replicasCount: number;
+    migrationsCount: number;
+  };
+};
 @observer
 class EndpointListItem extends React.Component<Props> {
   render() {
@@ -110,26 +113,29 @@ class EndpointListItem extends React.Component<Props> {
           <Image image={endpointImage} />
           <Title>
             <TitleLabel>{this.props.item.name}</TitleLabel>
-            <Subtitle>{this.props.item.description || 'N/A'}</Subtitle>
+            <Subtitle>{this.props.item.description || "N/A"}</Subtitle>
           </Title>
           <EndpointLogos height={42} endpoint={this.props.item.type} />
           <Created>
             <ItemLabel>Created</ItemLabel>
             <ItemValue>
-              {DateUtils.getLocalTime(this.props.item.created_at).format('DD MMMM YYYY, HH:mm')}
+              {DateUtils.getLocalTime(this.props.item.created_at).format(
+                "DD MMMM YYYY, HH:mm"
+              )}
             </ItemValue>
           </Created>
           <Usage>
             <ItemLabel>Usage</ItemLabel>
             <ItemValue>
-              {this.props.getUsage(this.props.item).migrationsCount} migrations,&nbsp;
+              {this.props.getUsage(this.props.item).migrationsCount}{" "}
+              migrations,&nbsp;
               {this.props.getUsage(this.props.item).replicasCount} replicas
             </ItemValue>
           </Usage>
         </Content>
       </Wrapper>
-    )
+    );
   }
 }
 
-export default EndpointListItem
+export default EndpointListItem;

+ 1 - 2
src/components/modules/EndpointModule/EndpointListItem/package.json

@@ -2,6 +2,5 @@
   "name": "EndpointListItem",
   "version": "0.0.0",
   "private": true,
-  "main":"./EndpointListItem.tsx"
+  "main": "./EndpointListItem.tsx"
 }
-

+ 37 - 31
src/components/modules/EndpointModule/EndpointListItem/story.tsx

@@ -12,50 +12,56 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import EndpointListItem from '.'
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import EndpointListItem from ".";
 
-storiesOf('EnpointListItem', module)
-  .add('openstack', () => (
+storiesOf("EnpointListItem", module)
+  .add("openstack", () => (
     <EndpointListItem
-      item={{
-        name: 'Endpoint 1',
-        type: 'openstack',
-        created_at: new Date(),
-        description: 'description',
-      } as any}
+      item={
+        {
+          name: "Endpoint 1",
+          type: "openstack",
+          created_at: new Date(),
+          description: "description",
+        } as any
+      }
       getUsage={() => ({ migrationsCount: 12, replicasCount: 2 })}
-      onClick={() => { }}
+      onClick={() => {}}
       selected={false}
       onSelectedChange={() => {}}
     />
   ))
-  .add('aws', () => (
+  .add("aws", () => (
     <EndpointListItem
-      item={{
-        name: 'Endpoint 1',
-        type: 'aws',
-        created_at: new Date(),
-        description: 'description',
-      } as any}
+      item={
+        {
+          name: "Endpoint 1",
+          type: "aws",
+          created_at: new Date(),
+          description: "description",
+        } as any
+      }
       getUsage={() => ({ migrationsCount: 12, replicasCount: 2 })}
-      onClick={() => { }}
+      onClick={() => {}}
       selected={false}
-      onSelectedChange={() => { }}
+      onSelectedChange={() => {}}
     />
   ))
-  .add('azure', () => (
+  .add("azure", () => (
     <EndpointListItem
-      item={{
-        name: 'Endpoint 1',
-        type: 'azure',
-        created_at: new Date(),
-        description: 'description',
-      } as any}
+      item={
+        {
+          name: "Endpoint 1",
+          type: "azure",
+          created_at: new Date(),
+          description: "description",
+        } as any
+      }
       getUsage={() => ({ migrationsCount: 12, replicasCount: 2 })}
-      onClick={() => { }}
+      onClick={() => {}}
       selected={false}
-      onSelectedChange={() => { }}
+      onSelectedChange={() => {}}
     />
-  ))
+  ));

+ 43 - 38
src/components/modules/EndpointModule/EndpointListItem/test.tsx

@@ -12,42 +12,47 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TestWrapper from '@src/utils/TestWrapper'
-import EndpointListItem from '.'
-
-const wrap = props => new TestWrapper(shallow(
-  
-  (<EndpointListItem {...props} />)
-), 'endpointListItem')
-
-describe('EndpointListItem Component', () => {
-  it('renders item properties', () => {
-    let wrapper = wrap({
-      item: { name: 'name-to-test', description: 'description-to-test' },
-      getUsage: () => { return {} },
-    })
-    expect(wrapper.findText('name')).toBe('name-to-test')
-    expect(wrapper.findText('description')).toBe('description-to-test')
-  })
-
-  it('renders usage count', () => {
-    let wrapper = wrap({
+import React from "react";
+import { shallow } from "enzyme";
+import sinon from "sinon";
+import TestWrapper from "@src/utils/TestWrapper";
+import EndpointListItem from ".";
+
+const wrap = props =>
+  new TestWrapper(shallow(<EndpointListItem {...props} />), "endpointListItem");
+
+describe("EndpointListItem Component", () => {
+  it("renders item properties", () => {
+    const wrapper = wrap({
+      item: { name: "name-to-test", description: "description-to-test" },
+      getUsage: () => {
+        return {};
+      },
+    });
+    expect(wrapper.findText("name")).toBe("name-to-test");
+    expect(wrapper.findText("description")).toBe("description-to-test");
+  });
+
+  it("renders usage count", () => {
+    const wrapper = wrap({
       item: {},
-      getUsage: () => { return { replicasCount: 12, migrationsCount: 11 } },
-    })
-    expect(wrapper.findText('usageCount')).toBe('11 migrations, 12 replicas')
-  })
-
-  it('dispatches onClick', () => {
-    let onClick = sinon.spy()
-    let wrapper = wrap({ item: { name: 't' }, getUsage: () => { return {} }, onClick })
-    wrapper.find('content-t').simulate('click')
-    expect(onClick.calledOnce).toBe(true)
-  })
-})
-
-
-
+      getUsage: () => {
+        return { replicasCount: 12, migrationsCount: 11 };
+      },
+    });
+    expect(wrapper.findText("usageCount")).toBe("11 migrations, 12 replicas");
+  });
+
+  it("dispatches onClick", () => {
+    const onClick = sinon.spy();
+    const wrapper = wrap({
+      item: { name: "t" },
+      getUsage: () => {
+        return {};
+      },
+      onClick,
+    });
+    wrapper.find("content-t").simulate("click");
+    expect(onClick.calledOnce).toBe(true);
+  });
+});

+ 44 - 36
src/components/modules/EndpointModule/EndpointLogos/EndpointLogos.tsx

@@ -12,83 +12,91 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { observer } from 'mobx-react'
-import styled, { css } from 'styled-components'
+import React from "react";
+import { observer } from "mobx-react";
+import styled, { css } from "styled-components";
 
-import configLoader from '@src/utils/Config'
-import Generic from './resources/Generic'
+import configLoader from "@src/utils/Config";
+import Generic from "./resources/Generic";
 
-const Wrapper = styled.div<any>``
+const Wrapper = styled.div<any>``;
 const Logo = styled.div<any>`
   display: flex;
   align-items: center;
   justify-content: center;
   width: ${props => props.width}px;
   height: ${props => props.height}px;
-  ${(props: any) => (props.url ? css`background: url('${props.url}') no-repeat center;` : '')}
+  ${(props: any) =>
+    props.url
+      ? css`
+          background: url("${props.url}") no-repeat center;
+        `
+      : ""}
   background-size: contain;
-`
+`;
 const widthHeights = [
   { w: 80, h: 32 },
   { w: 105, h: 42 },
   { w: 185, h: 128 },
   { w: 185, h: 64 },
-]
+];
 type Props = {
-  endpoint?: string | null,
-  height: number,
-  disabled?: boolean,
-  white?: boolean,
-  baseUrl?: string,
-  onClick?: () => void
-  style?: React.CSSProperties
-}
+  endpoint?: string | null;
+  height: number;
+  disabled?: boolean;
+  white?: boolean;
+  baseUrl?: string;
+  onClick?: () => void;
+  style?: React.CSSProperties;
+};
 @observer
 class EndpointLogos extends React.Component<Props> {
   static defaultProps: Props = {
     height: 64,
-  }
+  };
 
-  renderGenericLogo(size: { w: number, h: number }) {
+  renderGenericLogo(size: { w: number; h: number }) {
     return (
       <Generic
         size={size}
-        name={this.props.endpoint || ''}
+        name={this.props.endpoint || ""}
         disabled={this.props.disabled}
         white={this.props.white}
       />
-    )
+    );
   }
 
   render() {
-    const size = widthHeights.find(wh => wh.h === this.props.height)
+    const size = widthHeights.find(wh => wh.h === this.props.height);
 
     if (!size) {
-      return null
+      return null;
     }
 
-    let imageUrl: string | null = null
-    const provider = this.props.endpoint
-    if (provider && Object.keys(configLoader.config.providerNames).indexOf(provider) > -1) {
-      imageUrl = `${this.props.baseUrl || ''}/api/logos/${provider}/${size.h}`
-      const style = this.props.white ? 'white' : this.props.disabled ? 'disabled' : null
-      imageUrl = style ? `${imageUrl}/${style}` : imageUrl
+    let imageUrl: string | null = null;
+    const provider = this.props.endpoint;
+    if (
+      provider &&
+      Object.keys(configLoader.config.providerNames).indexOf(provider) > -1
+    ) {
+      imageUrl = `${this.props.baseUrl || ""}/api/logos/${provider}/${size.h}`;
+      const style = this.props.white
+        ? "white"
+        : this.props.disabled
+        ? "disabled"
+        : null;
+      imageUrl = style ? `${imageUrl}/${style}` : imageUrl;
     }
 
     return (
       // eslint-disable-next-line react/jsx-props-no-spreading
       <Wrapper {...this.props}>
-        <Logo
-          width={size.w}
-          height={size.h}
-          url={imageUrl}
-        >
+        <Logo width={size.w} height={size.h} url={imageUrl}>
           {imageUrl ? null : this.renderGenericLogo(size)}
         </Logo>
       </Wrapper>
-    )
+    );
   }
 }
 
-export default EndpointLogos
+export default EndpointLogos;

+ 1 - 2
src/components/modules/EndpointModule/EndpointLogos/package.json

@@ -2,6 +2,5 @@
   "name": "EndpointLogos",
   "version": "0.0.0",
   "private": true,
-  "main":"./EndpointLogos.tsx"
+  "main": "./EndpointLogos.tsx"
 }
-

+ 53 - 44
src/components/modules/EndpointModule/EndpointLogos/resources/Generic.tsx

@@ -12,13 +12,13 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import styled from 'styled-components'
+import React from "react";
+import styled from "styled-components";
 
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import generic64Image from './generic-64.svg'
-import generic128Image from './generic-128.svg'
-import generic128DisabledImage from './generic-128-disabled.svg'
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
+import generic64Image from "./generic-64.svg";
+import generic128Image from "./generic-128.svg";
+import generic128DisabledImage from "./generic-128-disabled.svg";
 
 const Wrapper = styled.div<any>`
   text-align: center;
@@ -28,92 +28,101 @@ const Wrapper = styled.div<any>`
   justify-content: center;
   align-items: center;
   letter-spacing: -1px;
-`
+`;
 const Logo = styled.div<any>`
   ${(props: any) => ThemeProps.exactWidth(`${props.width}px`)}
   ${(props: any) => ThemeProps.exactHeight(`${props.height}px`)}
   background: url(${(props: any) => props.image}) center no-repeat;
-`
+`;
 
 type Props = {
-  name: string,
-  size: { w: number, h: number },
-  disabled: boolean | null | undefined,
-  white: boolean | null | undefined,
-}
+  name: string;
+  size: { w: number; h: number };
+  disabled: boolean | null | undefined;
+  white: boolean | null | undefined;
+};
 class Generic extends React.Component<Props> {
   render32Generic(white: boolean | null | undefined) {
     return (
-      <Wrapper style={{
-        fontSize: '14px',
-        color: white ? 'white' : ThemePalette.grayscale[4],
-      }}
+      <Wrapper
+        style={{
+          fontSize: "14px",
+          color: white ? "white" : ThemePalette.grayscale[4],
+        }}
       >
         {this.props.name}
       </Wrapper>
-    )
+    );
   }
 
   render42Generic() {
     return (
-      <Wrapper style={{
-        fontSize: '18px',
-        color: ThemePalette.grayscale[4],
-      }}
+      <Wrapper
+        style={{
+          fontSize: "18px",
+          color: ThemePalette.grayscale[4],
+        }}
       >
         {this.props.name}
       </Wrapper>
-    )
+    );
   }
 
   render64Generic() {
     return (
-      <Wrapper style={{
-        fontSize: '22px',
-        color: ThemePalette.black,
-        textAlign: 'left',
-      }}
+      <Wrapper
+        style={{
+          fontSize: "22px",
+          color: ThemePalette.black,
+          textAlign: "left",
+        }}
       >
-        <Logo width={49} height={43} image={generic64Image} style={{ marginRight: '9px' }} />
+        <Logo
+          width={49}
+          height={43}
+          image={generic64Image}
+          style={{ marginRight: "9px" }}
+        />
         {this.props.name}
       </Wrapper>
-    )
+    );
   }
 
   render128Generic(disabled: boolean | null | undefined) {
     return (
-      <Wrapper style={{
-        fontSize: '22px',
-        color: disabled ? ThemePalette.grayscale[3] : ThemePalette.black,
-        textAlign: 'left',
-        flexDirection: 'column',
-      }}
+      <Wrapper
+        style={{
+          fontSize: "22px",
+          color: disabled ? ThemePalette.grayscale[3] : ThemePalette.black,
+          textAlign: "left",
+          flexDirection: "column",
+        }}
       >
         <Logo
           width={80}
           height={70}
           image={disabled ? generic128DisabledImage : generic128Image}
-          style={{ marginBottom: '4px' }}
+          style={{ marginBottom: "4px" }}
         />
         {this.props.name}
       </Wrapper>
-    )
+    );
   }
 
   render() {
     switch (this.props.size.h) {
       case 32:
-        return this.render32Generic(this.props.white)
+        return this.render32Generic(this.props.white);
       case 42:
-        return this.render42Generic()
+        return this.render42Generic();
       case 64:
-        return this.render64Generic()
+        return this.render64Generic();
       case 128:
-        return this.render128Generic(this.props.disabled)
+        return this.render128Generic(this.props.disabled);
       default:
-        return null
+        return null;
     }
   }
 }
 
-export default Generic
+export default Generic;

+ 47 - 54
src/components/modules/EndpointModule/EndpointLogos/story.tsx

@@ -12,28 +12,29 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import styled from 'styled-components'
-import EndpointLogos from '.'
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import styled from "styled-components";
+import EndpointLogos from ".";
 
 const Wrapper = styled.div<any>`
   display: flex;
   flex-wrap: wrap;
   margin-left: -32px;
   margin-top: -32px;
-  ${(props: any) => (props.background ? `background: ${props.background};` : '')}
+  ${(props: any) =>
+    props.background ? `background: ${props.background};` : ""}
 
   > div {
     margin-left: 32px;
     margin-top: 32px;
   }
-`
+`;
 const wrap = (opts: {
-  endpoint: string | null | undefined,
-  height: number | undefined,
-  disabled?: boolean,
-  white?: boolean,
+  endpoint: string | null | undefined;
+  height: number | undefined;
+  disabled?: boolean;
+  white?: boolean;
 }) => (
   <EndpointLogos
     endpoint={opts.endpoint as any}
@@ -42,66 +43,58 @@ const wrap = (opts: {
     white={opts.white}
     baseUrl="http://localhost:3000"
   />
-)
+);
 const providers = [
-  'aws',
-  'azure',
-  'opc',
-  'openstack',
-  'oracle_vm',
-  'oci',
-  'vmware_vsphere',
-  'Generic Cloud',
-  'hyper-v',
-  'scvmm',
-]
+  "aws",
+  "azure",
+  "opc",
+  "openstack",
+  "oracle_vm",
+  "oci",
+  "vmware_vsphere",
+  "Generic Cloud",
+  "hyper-v",
+  "scvmm",
+];
 
-storiesOf('EndpointLogos', module)
-  .add('32px', () => {
-    const height = 32
+storiesOf("EndpointLogos", module)
+  .add("32px", () => {
+    const height = 32;
     return (
-      <Wrapper>
-        {providers.map(p => wrap({ endpoint: p, height }))}
-      </Wrapper>
-    )
+      <Wrapper>{providers.map(p => wrap({ endpoint: p, height }))}</Wrapper>
+    );
   })
-  .add('32px - white', () => {
-    const height = 32
+  .add("32px - white", () => {
+    const height = 32;
     return (
       <Wrapper background="#202134">
         {providers.map(p => wrap({ endpoint: p, height, white: true }))}
       </Wrapper>
-    )
+    );
   })
-  .add('42px', () => {
-    const height = 42
+  .add("42px", () => {
+    const height = 42;
     return (
-      <Wrapper>
-        {providers.map(p => wrap({ endpoint: p, height }))}
-      </Wrapper>
-    )
+      <Wrapper>{providers.map(p => wrap({ endpoint: p, height }))}</Wrapper>
+    );
   })
-  .add('64px', () => {
-    const height = 64
+  .add("64px", () => {
+    const height = 64;
     return (
-      <Wrapper>
-        {providers.map(p => wrap({ endpoint: p, height }))}
-      </Wrapper>
-    )
+      <Wrapper>{providers.map(p => wrap({ endpoint: p, height }))}</Wrapper>
+    );
   })
-  .add('128px', () => {
-    const height = 128
+  .add("128px", () => {
+    const height = 128;
     return (
-      <Wrapper>
-        {providers.map(p => wrap({ endpoint: p, height }))}
-      </Wrapper>
-    )
+      <Wrapper>{providers.map(p => wrap({ endpoint: p, height }))}</Wrapper>
+    );
   })
-  .add('128px - disabled', () => {
-    const height = 128
+  .add("128px - disabled", () => {
+    const height = 128;
     return (
       <Wrapper>
         {providers.map(p => wrap({ endpoint: p, height, disabled: true }))}
       </Wrapper>
-    )
-  })
+    );
+  });

+ 28 - 30
src/components/modules/EndpointModule/EndpointLogos/test.tsx

@@ -12,33 +12,31 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { shallow } from 'enzyme'
-import TestWrapper from '@src/utils/TestWrapper'
-import EndpointLogos from '.'
-
-const wrap = props => new TestWrapper(shallow(<EndpointLogos {...props} />), 'endpointLogos')
-
-describe('EndpointLogos Component', () => {
-  it('renders 32px aws', () => {
-    const wrapper = wrap({ height: 32, endpoint: 'aws' })
-    const logo = wrapper.find('logo')
-    expect(logo.prop('url')).toBe('/api/logos/aws/32')
-  })
-
-  it('renders 128px azure disabled', () => {
-    const wrapper = wrap({ height: 128, endpoint: 'azure', disabled: true })
-    const logo = wrapper.find('logo')
-    expect(logo.prop('url')).toBe('/api/logos/azure/128/disabled')
-  })
-
-  it('renders 64px generic logo', () => {
-    const wrapper = wrap({ height: 64, endpoint: 'generic' })
-    const logo = wrapper.find('genericLogo')
-    expect(logo.prop('name')).toBe('generic')
-    expect(logo.prop('size').h).toBe(64)
-  })
-})
-
-
-
+import React from "react";
+import { shallow } from "enzyme";
+import TestWrapper from "@src/utils/TestWrapper";
+import EndpointLogos from ".";
+
+const wrap = props =>
+  new TestWrapper(shallow(<EndpointLogos {...props} />), "endpointLogos");
+
+describe("EndpointLogos Component", () => {
+  it("renders 32px aws", () => {
+    const wrapper = wrap({ height: 32, endpoint: "aws" });
+    const logo = wrapper.find("logo");
+    expect(logo.prop("url")).toBe("/api/logos/aws/32");
+  });
+
+  it("renders 128px azure disabled", () => {
+    const wrapper = wrap({ height: 128, endpoint: "azure", disabled: true });
+    const logo = wrapper.find("logo");
+    expect(logo.prop("url")).toBe("/api/logos/azure/128/disabled");
+  });
+
+  it("renders 64px generic logo", () => {
+    const wrapper = wrap({ height: 64, endpoint: "generic" });
+    const logo = wrapper.find("genericLogo");
+    expect(logo.prop("name")).toBe("generic");
+    expect(logo.prop("size").h).toBe(64);
+  });
+});

+ 253 - 200
src/components/modules/EndpointModule/EndpointModal/EndpointModal.tsx

@@ -12,30 +12,30 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import styled from 'styled-components'
-import { observer } from 'mobx-react'
-import { observe } from 'mobx'
-
-import EndpointLogos from '@src/components/modules/EndpointModule/EndpointLogos'
-import StatusIcon from '@src/components/ui/StatusComponents/StatusIcon'
-import CopyButton from '@src/components/ui/CopyButton'
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
-import Button from '@src/components/ui/Button'
-import LoadingButton from '@src/components/ui/LoadingButton'
-
-import type { Endpoint as EndpointType } from '@src/@types/Endpoint'
-import type { Field } from '@src/@types/Field'
-import notificationStore from '@src/stores/NotificationStore'
-import endpointStore from '@src/stores/EndpointStore'
-import providerStore from '@src/stores/ProviderStore'
-import ObjectUtils from '@src/utils/ObjectUtils'
-import { ThemePalette } from '@src/components/Theme'
-import DomUtils from '@src/utils/DomUtils'
-import { ContentPlugin } from '@src/plugins'
-import DefaultContentPlugin from '@src/plugins/default/ContentPlugin'
-import KeyboardManager from '@src/utils/KeyboardManager'
-import { ProviderTypes } from '@src/@types/Providers'
+import React from "react";
+import styled from "styled-components";
+import { observer } from "mobx-react";
+import { observe } from "mobx";
+
+import EndpointLogos from "@src/components/modules/EndpointModule/EndpointLogos";
+import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon";
+import CopyButton from "@src/components/ui/CopyButton";
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
+import Button from "@src/components/ui/Button";
+import LoadingButton from "@src/components/ui/LoadingButton";
+
+import type { Endpoint as EndpointType } from "@src/@types/Endpoint";
+import type { Field } from "@src/@types/Field";
+import notificationStore from "@src/stores/NotificationStore";
+import endpointStore from "@src/stores/EndpointStore";
+import providerStore from "@src/stores/ProviderStore";
+import ObjectUtils from "@src/utils/ObjectUtils";
+import { ThemePalette } from "@src/components/Theme";
+import DomUtils from "@src/utils/DomUtils";
+import { ContentPlugin } from "@src/plugins";
+import DefaultContentPlugin from "@src/plugins/default/ContentPlugin";
+import KeyboardManager from "@src/utils/KeyboardManager";
+import { ProviderTypes } from "@src/@types/Providers";
 
 const Wrapper = styled.div<any>`
   padding: 48px 0 32px 0;
@@ -43,29 +43,29 @@ const Wrapper = styled.div<any>`
   align-items: center;
   flex-direction: column;
   min-height: 0;
-`
+`;
 const Status = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
   flex-shrink: 0;
-`
+`;
 const StatusHeader = styled.div<any>`
   display: flex;
   align-items: center;
-`
+`;
 const StatusMessage = styled.div<any>`
   margin-left: 8px;
   display: flex;
   align-items: center;
   line-height: 12px;
-`
+`;
 const ShowErrorButton = styled.span`
   font-size: 10px;
   color: ${ThemePalette.primary};
   margin-left: 8px;
   cursor: pointer;
-`
+`;
 const StatusError = styled.div<any>`
   max-width: 100%;
   margin: 16px 16px 0 16px;
@@ -80,52 +80,52 @@ const StatusError = styled.div<any>`
     background-position-y: 4px;
     margin-left: 4px;
   }
-`
+`;
 const Content = styled.div<any>`
   width: 100%;
   display: flex;
   flex-direction: column;
   min-height: 0;
-`
+`;
 const LoadingWrapper = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
   margin: 32px 0;
-`
+`;
 const LoadingText = styled.div<any>`
   font-size: 18px;
   margin-top: 32px;
-`
+`;
 const Buttons = styled.div<any>`
   display: flex;
   justify-content: space-between;
   margin-top: 32px;
   flex-shrink: 0;
   padding: 0 32px;
-`
+`;
 
 type Props = {
-  type?: ProviderTypes | null,
-  cancelButtonText: string,
-  deleteOnCancel?: boolean,
-  endpoint?: EndpointType | null,
-  isNewEndpoint?: boolean,
-  onCancelClick: (opts?: { autoClose?: boolean }) => void,
-  onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
-}
+  type?: ProviderTypes | null;
+  cancelButtonText: string;
+  deleteOnCancel?: boolean;
+  endpoint?: EndpointType | null;
+  isNewEndpoint?: boolean;
+  onCancelClick: (opts?: { autoClose?: boolean }) => void;
+  onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void;
+};
 type State = {
-  invalidFields: any[],
-  validating: boolean,
-  showErrorMessage: boolean,
-  endpoint: EndpointType | null,
-  isNew: boolean | null,
-}
+  invalidFields: any[];
+  validating: boolean;
+  showErrorMessage: boolean;
+  endpoint: EndpointType | null;
+  isNew: boolean | null;
+};
 @observer
 class EndpointModal extends React.Component<Props, State> {
   static defaultProps = {
-    cancelButtonText: 'Cancel',
-  }
+    cancelButtonText: "Cancel",
+  };
 
   state: State = {
     invalidFields: [],
@@ -133,68 +133,82 @@ class EndpointModal extends React.Component<Props, State> {
     showErrorMessage: false,
     endpoint: null,
     isNew: null,
-  }
+  };
 
-  scrollableRef!: HTMLElement
+  scrollableRef!: HTMLElement;
 
-  closeTimeout: number | undefined
+  closeTimeout: number | undefined;
 
-  contentPluginRef!: DefaultContentPlugin
+  contentPluginRef!: DefaultContentPlugin;
 
-  isValidateButtonEnabled: boolean = false
+  isValidateButtonEnabled = false;
 
-  providerStoreObserver!: () => void
+  providerStoreObserver!: () => void;
 
-  endpointValidationObserver!: () => void
+  endpointValidationObserver!: () => void;
 
   UNSAFE_componentWillMount() {
-    this.UNSAFE_componentWillReceiveProps(this.props)
-    this.providerStoreObserver = observe(providerStore, 'connectionInfoSchema', () => {
-      if (this.props.onResizeUpdate) this.props.onResizeUpdate(this.scrollableRef)
-    })
-    this.endpointValidationObserver = observe(endpointStore, 'validation', () => {
-      this.UNSAFE_componentWillReceiveProps(this.props)
-    })
+    this.UNSAFE_componentWillReceiveProps(this.props);
+    this.providerStoreObserver = observe(
+      providerStore,
+      "connectionInfoSchema",
+      () => {
+        if (this.props.onResizeUpdate)
+          this.props.onResizeUpdate(this.scrollableRef);
+      }
+    );
+    this.endpointValidationObserver = observe(
+      endpointStore,
+      "validation",
+      () => {
+        this.UNSAFE_componentWillReceiveProps(this.props);
+      }
+    );
   }
 
   componentDidMount() {
     const loadSchema = async () => {
       if (!this.endpointType) {
-        return
+        return;
       }
-      await providerStore.getConnectionInfoSchema(this.endpointType)
-      this.fillRequiredDefaults()
-    }
-    loadSchema()
-    KeyboardManager.onEnter('endpoint', () => {
-      if (this.isValidateButtonEnabled) this.handleValidateClick()
-    }, 2)
+      await providerStore.getConnectionInfoSchema(this.endpointType);
+      this.fillRequiredDefaults();
+    };
+    loadSchema();
+    KeyboardManager.onEnter(
+      "endpoint",
+      () => {
+        if (this.isValidateButtonEnabled) this.handleValidateClick();
+      },
+      2
+    );
   }
 
   UNSAFE_componentWillReceiveProps(props: Props) {
     if (this.state.validating) {
       if (endpointStore.validation && !endpointStore.validation.valid) {
-        this.setState({ validating: false })
+        this.setState({ validating: false });
       }
     }
 
     if (props.endpoint && endpointStore.connectionInfo) {
-      const plugin: any = ContentPlugin.for(props.endpoint.type)
+      const plugin: any = ContentPlugin.for(props.endpoint.type);
       this.setState(prevState => ({
         isNew: this.props.isNewEndpoint
-          ? (prevState.isNew === null || prevState.isNew) : prevState.isNew,
+          ? prevState.isNew === null || prevState.isNew
+          : prevState.isNew,
         endpoint: {
           ...prevState.endpoint,
           ...ObjectUtils.flatten(
             props.endpoint || {},
-            plugin.REQUIRES_PARENT_OBJECT_PATH,
+            plugin.REQUIRES_PARENT_OBJECT_PATH
           ),
           ...ObjectUtils.flatten(
             endpointStore.connectionInfo || {},
-            plugin.REQUIRES_PARENT_OBJECT_PATH,
+            plugin.REQUIRES_PARENT_OBJECT_PATH
           ),
         },
-      }))
+      }));
     } else {
       this.setState(prevState => ({
         isNew: prevState.isNew === null || prevState.isNew,
@@ -202,187 +216,204 @@ class EndpointModal extends React.Component<Props, State> {
           type: props.type,
           ...ObjectUtils.flatten(prevState.endpoint || {}),
         },
-      }))
+      }));
     }
 
-    if (props.onResizeUpdate) props.onResizeUpdate(this.scrollableRef)
+    if (props.onResizeUpdate) props.onResizeUpdate(this.scrollableRef);
   }
 
   componentWillUnmount() {
-    endpointStore.clearValidation()
-    providerStore.clearConnectionInfoSchema()
-    clearTimeout(this.closeTimeout)
-    KeyboardManager.removeKeyDown('endpoint')
-    this.providerStoreObserver()
-    this.endpointValidationObserver()
+    endpointStore.clearValidation();
+    providerStore.clearConnectionInfoSchema();
+    clearTimeout(this.closeTimeout);
+    KeyboardManager.removeKeyDown("endpoint");
+    this.providerStoreObserver();
+    this.endpointValidationObserver();
   }
 
   get endpointType() {
     if (this.props.endpoint) {
-      return this.props.endpoint.type
+      return this.props.endpoint.type;
     }
 
-    return this.props.type
+    return this.props.type;
   }
 
   getFieldValue(field: Field | null) {
     if (!field || !this.state.endpoint) {
-      return ''
+      return "";
     }
     if (this.state.endpoint[field.name] != null) {
-      return this.state.endpoint[field.name]
+      return this.state.endpoint[field.name];
     }
 
-    if (Object.keys(field).find(k => k === 'default')) {
-      return field.default
+    if (Object.keys(field).find(k => k === "default")) {
+      return field.default;
     }
 
-    if (field.type === 'integer') {
-      return null
+    if (field.type === "integer") {
+      return null;
     }
-    return ''
+    return "";
   }
 
   fillRequiredDefaults() {
     this.setState(prevState => {
-      const endpoint: any = { ...prevState.endpoint }
-      const requiredFieldsDefaults = providerStore.connectionInfoSchema
-        .filter(f => f.required && f.default != null)
+      const endpoint: any = { ...prevState.endpoint };
+      const requiredFieldsDefaults = providerStore.connectionInfoSchema.filter(
+        f => f.required && f.default != null
+      );
       requiredFieldsDefaults.forEach(f => {
         if (endpoint[f.name] == null) {
-          endpoint[f.name] = f.default
+          endpoint[f.name] = f.default;
         }
-      })
-      return { endpoint }
-    })
+      });
+      return { endpoint };
+    });
   }
 
-  handleFieldsChange(items: { field: Field, value: any }[]) {
+  handleFieldsChange(items: { field: Field; value: any }[]) {
     this.setState(prevState => {
-      const endpoint: any = { ...prevState.endpoint }
+      const endpoint: any = { ...prevState.endpoint };
 
       items.forEach(item => {
-        let value = item.value
-        if (item.field.type === 'array') {
-          const arrayItems = endpoint[item.field.name] || []
+        let value = item.value;
+        if (item.field.type === "array") {
+          const arrayItems = endpoint[item.field.name] || [];
           value = arrayItems.find((v: any) => v === item.value)
-            ? arrayItems.filter((v: any) => v !== item.value) : [...arrayItems, item.value]
+            ? arrayItems.filter((v: any) => v !== item.value)
+            : [...arrayItems, item.value];
         }
 
-        endpoint[item.field.name] = value
-      })
+        endpoint[item.field.name] = value;
+      });
 
-      return { endpoint }
-    })
+      return { endpoint };
+    });
   }
 
   handleValidateClick() {
     if (!this.highlightRequired()) {
-      this.setState({ validating: true })
+      this.setState({ validating: true });
 
-      notificationStore.alert('Saving endpoint ...')
-      endpointStore.clearValidation()
+      notificationStore.alert("Saving endpoint ...");
+      endpointStore.clearValidation();
 
       if (this.state.isNew) {
-        this.add()
+        this.add();
       } else {
-        this.update()
+        this.update();
       }
     } else {
-      notificationStore.alert('Please fill all the required fields', 'error')
+      notificationStore.alert("Please fill all the required fields", "error");
     }
   }
 
   handleShowErrorMessageClick() {
-    this.setState(prevState => ({ showErrorMessage: !prevState.showErrorMessage }), () => {
-      if (this.props.onResizeUpdate) this.props.onResizeUpdate(this.scrollableRef)
-    })
+    this.setState(
+      prevState => ({ showErrorMessage: !prevState.showErrorMessage }),
+      () => {
+        if (this.props.onResizeUpdate)
+          this.props.onResizeUpdate(this.scrollableRef);
+      }
+    );
   }
 
   handleCopyErrorMessageClick() {
     if (!endpointStore.validation) {
-      return
+      return;
     }
 
-    const succesful = DomUtils.copyTextToClipboard(endpointStore.validation.message)
+    const succesful = DomUtils.copyTextToClipboard(
+      endpointStore.validation.message
+    );
 
     if (succesful) {
-      notificationStore.alert('The message has been copied to clipboard.')
+      notificationStore.alert("The message has been copied to clipboard.");
     }
   }
 
   handleCancelClick() {
     if (this.props.deleteOnCancel && this.state.isNew === false) {
-      endpointStore.delete(endpointStore.endpoints[0])
+      endpointStore.delete(endpointStore.endpoints[0]);
     }
-    this.props.onCancelClick()
+    this.props.onCancelClick();
   }
 
   highlightRequired() {
-    const invalidFields = this.contentPluginRef.findInvalidFields()
-    this.setState({ invalidFields })
-    return invalidFields.length > 0
+    const invalidFields = this.contentPluginRef.findInvalidFields();
+    this.setState({ invalidFields });
+    return invalidFields.length > 0;
   }
 
   async update() {
-    const stateEndpoint = this.state.endpoint
+    const stateEndpoint = this.state.endpoint;
     if (!stateEndpoint) {
-      return
+      return;
     }
-    const endpoint = endpointStore.endpoints.find(e => e.id === stateEndpoint.id)
+    const endpoint = endpointStore.endpoints.find(
+      e => e.id === stateEndpoint.id
+    );
     if (!endpoint) {
-      throw new Error('Endpoint not found in store')
+      throw new Error("Endpoint not found in store");
     }
-    await endpointStore.update(stateEndpoint)
+    await endpointStore.update(stateEndpoint);
 
-    this.setState({ endpoint: ObjectUtils.flatten(endpoint) })
-    notificationStore.alert('Validating endpoint ...')
-    endpointStore.validate(endpoint)
+    this.setState({ endpoint: ObjectUtils.flatten(endpoint) });
+    notificationStore.alert("Validating endpoint ...");
+    endpointStore.validate(endpoint);
   }
 
   async add() {
     if (!this.state.endpoint) {
-      return
+      return;
     }
 
-    await endpointStore.add(this.state.endpoint)
-    const endpoint = endpointStore.endpoints[0]
-    this.setState({ isNew: false, endpoint: ObjectUtils.flatten(endpoint) })
-    notificationStore.alert('Validating endpoint ...')
-    endpointStore.validate(endpoint)
+    await endpointStore.add(this.state.endpoint);
+    const endpoint = endpointStore.endpoints[0];
+    this.setState({ isNew: false, endpoint: ObjectUtils.flatten(endpoint) });
+    notificationStore.alert("Validating endpoint ...");
+    endpointStore.validate(endpoint);
   }
 
   renderEndpointStatus() {
-    const validation = endpointStore.validation
+    const validation = endpointStore.validation;
     if (!this.state.validating && !validation) {
-      return null
+      return null;
     }
 
-    let status = 'RUNNING'
-    let message = 'Validating Endpoint ...'
-    let error = null
-    let showErrorButton = null
+    let status = "RUNNING";
+    let message = "Validating Endpoint ...";
+    let error = null;
+    let showErrorButton = null;
 
     if (validation) {
       if (validation.valid) {
-        message = 'Endpoint is Valid'
-        status = 'COMPLETED'
+        message = "Endpoint is Valid";
+        status = "COMPLETED";
       } else {
-        status = 'ERROR'
-        message = 'Validation failed'
+        status = "ERROR";
+        message = "Validation failed";
         if (validation.message) {
           showErrorButton = (
-            <ShowErrorButton onClick={() => { this.handleShowErrorMessageClick() }}>
-              {this.state.showErrorMessage ? 'Hide' : 'Show'} Error
+            <ShowErrorButton
+              onClick={() => {
+                this.handleShowErrorMessageClick();
+              }}
+            >
+              {this.state.showErrorMessage ? "Hide" : "Show"} Error
             </ShowErrorButton>
-          )
-          error = this.state.showErrorMessage
-            ? (
-              <StatusError
-                onClick={() => { this.handleCopyErrorMessageClick() }}
-              >{validation.message}<CopyButton />
-              </StatusError>
-            ) : null
+          );
+          error = this.state.showErrorMessage ? (
+            <StatusError
+              onClick={() => {
+                this.handleCopyErrorMessageClick();
+              }}
+            >
+              {validation.message}
+              <CopyButton />
+            </StatusError>
+          ) : null;
         }
       }
     }
@@ -391,31 +422,35 @@ class EndpointModal extends React.Component<Props, State> {
       <Status>
         <StatusHeader>
           <StatusIcon status={status} />
-          <StatusMessage>{message}{showErrorButton}</StatusMessage>
+          <StatusMessage>
+            {message}
+            {showErrorButton}
+          </StatusMessage>
         </StatusHeader>
         {error}
       </Status>
-    )
+    );
   }
 
   renderButtons() {
-    this.isValidateButtonEnabled = true
+    this.isValidateButtonEnabled = true;
     let actionButton = (
-      <Button
-        large
-        onClick={() => this.handleValidateClick()}
-      >Validate and save
+      <Button large onClick={() => this.handleValidateClick()}>
+        Validate and save
       </Button>
-    )
+    );
 
-    let message = 'Validating Endpoint ...'
-    if (this.state.validating || (endpointStore.validation && endpointStore.validation.valid)) {
+    let message = "Validating Endpoint ...";
+    if (
+      this.state.validating ||
+      (endpointStore.validation && endpointStore.validation.valid)
+    ) {
       if (endpointStore.validation && endpointStore.validation.valid) {
-        message = 'Saving ...'
+        message = "Saving ...";
       }
 
-      this.isValidateButtonEnabled = false
-      actionButton = <LoadingButton large>{message}</LoadingButton>
+      this.isValidateButtonEnabled = false;
+      actionButton = <LoadingButton large>{message}</LoadingButton>;
     }
 
     return (
@@ -424,24 +459,25 @@ class EndpointModal extends React.Component<Props, State> {
           large
           secondary
           onClick={() => {
-            this.handleCancelClick()
+            this.handleCancelClick();
           }}
-        >{this.props.cancelButtonText}
+        >
+          {this.props.cancelButtonText}
         </Button>
         {actionButton}
       </Buttons>
-    )
+    );
   }
 
   renderContent() {
     if (providerStore.connectionSchemaLoading || !this.endpointType) {
-      return null
+      return null;
     }
-    const contentElement: any = ContentPlugin.for(this.endpointType)
+    const contentElement: any = ContentPlugin.for(this.endpointType);
     return (
       <Content>
         {/* Fix browsers autofilling password fields */}
-        <div style={{ position: 'absolute', left: '-10000px' }}>
+        <div style={{ position: "absolute", left: "-10000px" }}>
           <input name="username" type="text" />
           <input name="password" type="password" />
         </div>
@@ -455,31 +491,41 @@ class EndpointModal extends React.Component<Props, State> {
           cancelButtonText: this.props.cancelButtonText,
           originalConnectionInfo: endpointStore.connectionInfo,
           getFieldValue: (field: Field | null) => this.getFieldValue(field),
-          highlightRequired: () => { this.highlightRequired() },
+          highlightRequired: () => {
+            this.highlightRequired();
+          },
           handleFieldChange: (field: Field | null, value: any) => {
-            if (field) this.handleFieldsChange([{ field, value }])
+            if (field) this.handleFieldsChange([{ field, value }]);
           },
           handleFieldsChange: (fields: { field: Field; value: any }[]) => {
-            this.handleFieldsChange(fields)
+            this.handleFieldsChange(fields);
+          },
+          handleValidateClick: () => {
+            this.handleValidateClick();
+          },
+          handleCancelClick: () => {
+            this.handleCancelClick();
+          },
+          scrollableRef: (ref: HTMLElement) => {
+            this.scrollableRef = ref;
+          },
+          onRef: (ref: DefaultContentPlugin) => {
+            this.contentPluginRef = ref;
           },
-          handleValidateClick: () => { this.handleValidateClick() },
-          handleCancelClick: () => { this.handleCancelClick() },
-          scrollableRef: (ref: HTMLElement) => { this.scrollableRef = ref },
-          onRef: (ref: DefaultContentPlugin) => { this.contentPluginRef = ref },
           onResizeUpdate: (scrollOffset: number) => {
             if (this.props.onResizeUpdate) {
-              this.props.onResizeUpdate(this.scrollableRef, scrollOffset)
+              this.props.onResizeUpdate(this.scrollableRef, scrollOffset);
             }
           },
         })}
         {this.renderButtons()}
       </Content>
-    )
+    );
   }
 
   renderLoading() {
     if (!providerStore.connectionSchemaLoading) {
-      return null
+      return null;
     }
 
     return (
@@ -487,25 +533,32 @@ class EndpointModal extends React.Component<Props, State> {
         <StatusImage loading />
         <LoadingText>Loading connection schema ...</LoadingText>
       </LoadingWrapper>
-    )
+    );
   }
 
   render() {
-    if (endpointStore.validation && endpointStore.validation.valid
-      && !this.closeTimeout) {
+    if (
+      endpointStore.validation &&
+      endpointStore.validation.valid &&
+      !this.closeTimeout
+    ) {
       this.closeTimeout = window.setTimeout(() => {
-        this.props.onCancelClick({ autoClose: true })
-      }, 2000)
+        this.props.onCancelClick({ autoClose: true });
+      }, 2000);
     }
 
     return (
       <Wrapper>
-        <EndpointLogos style={{ marginBottom: '16px' }} height={128} endpoint={this.endpointType} />
+        <EndpointLogos
+          style={{ marginBottom: "16px" }}
+          height={128}
+          endpoint={this.endpointType}
+        />
         {this.renderContent()}
         {this.renderLoading()}
       </Wrapper>
-    )
+    );
   }
 }
 
-export default EndpointModal
+export default EndpointModal;

+ 71 - 47
src/components/modules/EndpointModule/EndpointValidation/EndpointValidation.tsx

@@ -12,52 +12,52 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { observer } from 'mobx-react'
-import styled, { css } from 'styled-components'
+import React from "react";
+import { observer } from "mobx-react";
+import styled, { css } from "styled-components";
 
-import Button from '@src/components/ui/Button'
-import CopyButton from '@src/components/ui/CopyButton'
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
+import Button from "@src/components/ui/Button";
+import CopyButton from "@src/components/ui/CopyButton";
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
 
-import { ThemePalette } from '@src/components/Theme'
-import type { Validation as ValidationType } from '@src/@types/Endpoint'
+import { ThemePalette } from "@src/components/Theme";
+import type { Validation as ValidationType } from "@src/@types/Endpoint";
 
-import notificationStore from '@src/stores/NotificationStore'
-import DomUtils from '@src/utils/DomUtils'
+import notificationStore from "@src/stores/NotificationStore";
+import DomUtils from "@src/utils/DomUtils";
 
 const Wrapper = styled.div<any>`
   padding: 48px 32px 32px 32px;
-`
+`;
 const contentStyle = css`
   display: flex;
   flex-direction: column;
   align-items: center;
-`
+`;
 const Loading = styled.div<any>`
   ${contentStyle}
-`
+`;
 const Validation = styled.div<any>`
   ${contentStyle}
-`
+`;
 const Message = styled.div<any>`
   max-width: 100%;
   overflow: auto;
   margin-top: 48px;
   text-align: center;
-`
+`;
 const Title = styled.div<any>`
   font-size: 18px;
   margin-bottom: 8px;
-`
+`;
 const Subtitle = styled.div<any>`
   color: ${ThemePalette.grayscale[4]};
-`
+`;
 const Buttons = styled.div<any>`
   margin-top: 48px;
   display: flex;
-  justify-content: ${props => (props.center ? 'center' : 'space-between')};
-`
+  justify-content: ${props => (props.center ? "center" : "space-between")};
+`;
 const Error = styled.div<any>`
   text-align: left;
   cursor: pointer;
@@ -69,29 +69,29 @@ const Error = styled.div<any>`
     background-position-y: 4px;
     margin-left: 4px;
   }
-`
+`;
 
 type Props = {
-  loading: boolean,
-  validation?: ValidationType | null,
-  onCancelClick: () => void,
-  onRetryClick: () => void,
-}
+  loading: boolean;
+  validation?: ValidationType | null;
+  onCancelClick: () => void;
+  onRetryClick: () => void;
+};
 @observer
 class EndpointValidation extends React.Component<Props> {
   handleCopyClick(message: string) {
-    const succesful = DomUtils.copyTextToClipboard(message)
+    const succesful = DomUtils.copyTextToClipboard(message);
 
     if (succesful) {
-      notificationStore.alert('The value has been copied to clipboard.')
+      notificationStore.alert("The value has been copied to clipboard.");
     } else {
-      notificationStore.alert('The value couldn\'t be copied', 'error')
+      notificationStore.alert("The value couldn't be copied", "error");
     }
   }
 
   renderLoading() {
     if (!this.props.loading) {
-      return null
+      return null;
     }
 
     return (
@@ -102,12 +102,16 @@ class EndpointValidation extends React.Component<Props> {
           <Subtitle>Please wait ...</Subtitle>
         </Message>
       </Loading>
-    )
+    );
   }
 
   renderSuccessValidationMessage() {
-    if (!this.props.validation || !this.props.validation.valid || this.props.loading) {
-      return null
+    if (
+      !this.props.validation ||
+      !this.props.validation.valid ||
+      this.props.loading
+    ) {
+      return null;
     }
 
     return (
@@ -118,44 +122,64 @@ class EndpointValidation extends React.Component<Props> {
           <Subtitle>All tests passed succesfully.</Subtitle>
         </Message>
       </Validation>
-    )
+    );
   }
 
   renderFailedValidationMessage() {
-    if (!this.props.validation || this.props.validation.valid || this.props.loading) {
-      return null
+    if (
+      !this.props.validation ||
+      this.props.validation.valid ||
+      this.props.loading
+    ) {
+      return null;
     }
 
-    const message = this.props.validation.message || 'An unexpected error occurred.'
+    const message =
+      this.props.validation.message || "An unexpected error occurred.";
 
     return (
       <Validation>
         <StatusImage status="ERROR" />
         <Message>
           <Title>Validation Failed</Title>
-          <Error onClick={() => { this.handleCopyClick(message) }}>
-            {message}<CopyButton />
+          <Error
+            onClick={() => {
+              this.handleCopyClick(message);
+            }}
+          >
+            {message}
+            <CopyButton />
           </Error>
         </Message>
       </Validation>
-    )
+    );
   }
 
   renderButtons() {
-    if (!this.props.loading && this.props.validation && this.props.validation.valid) {
+    if (
+      !this.props.loading &&
+      this.props.validation &&
+      this.props.validation.valid
+    ) {
       return (
         <Buttons center>
-          <Button secondary onClick={this.props.onCancelClick}>Dismiss</Button>
+          <Button secondary onClick={this.props.onCancelClick}>
+            Dismiss
+          </Button>
         </Buttons>
-      )
+      );
     }
 
     return (
       <Buttons>
-        <Button secondary onClick={this.props.onCancelClick}>Cancel</Button>
-        <Button disabled={this.props.loading} onClick={this.props.onRetryClick}>Retry</Button>
+        <Button secondary onClick={this.props.onCancelClick}>
+          Cancel
+        </Button>
+        <Button disabled={this.props.loading} onClick={this.props.onRetryClick}>
+          Retry
+        </Button>
       </Buttons>
-    )
+    );
   }
 
   render() {
@@ -166,8 +190,8 @@ class EndpointValidation extends React.Component<Props> {
         {this.renderSuccessValidationMessage()}
         {this.renderButtons()}
       </Wrapper>
-    )
+    );
   }
 }
 
-export default EndpointValidation
+export default EndpointValidation;

+ 1 - 2
src/components/modules/EndpointModule/EndpointValidation/package.json

@@ -2,6 +2,5 @@
   "name": "EndpointValidation",
   "version": "0.0.0",
   "private": true,
-  "main":"./EndpointValidation.tsx"
+  "main": "./EndpointValidation.tsx"
 }
-

+ 25 - 14
src/components/modules/EndpointModule/EndpointValidation/story.tsx

@@ -14,21 +14,32 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 /* eslint-disable react/jsx-props-no-spreading */
 
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import EndpointValidation from '.'
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import EndpointValidation from ".";
 
-const props: any = {}
-storiesOf('EndpointValidation', module)
-  .add('validating', () => (
-    <div style={{ width: '526px' }}><EndpointValidation loading {...props} /></div>
+const props: any = {};
+storiesOf("EndpointValidation", module)
+  .add("validating", () => (
+    <div style={{ width: "526px" }}>
+      <EndpointValidation loading {...props} />
+    </div>
   ))
-  .add('valid', () => (
-    <div style={{ width: '526px' }}><EndpointValidation validation={{ valid: true }} {...props} /></div>
+  .add("valid", () => (
+    <div style={{ width: "526px" }}>
+      <EndpointValidation validation={{ valid: true }} {...props} />
+    </div>
   ))
-  .add('failed', () => (
-    <div style={{ width: '526px' }}><EndpointValidation validation={{}} {...props} /></div>
-  ))
-  .add('failed custom message', () => (
-    <div style={{ width: '526px' }}><EndpointValidation validation={{ message: 'Failed because of reasons' }} {...props} /></div>
+  .add("failed", () => (
+    <div style={{ width: "526px" }}>
+      <EndpointValidation validation={{}} {...props} />
+    </div>
   ))
+  .add("failed custom message", () => (
+    <div style={{ width: "526px" }}>
+      <EndpointValidation
+        validation={{ message: "Failed because of reasons" }}
+        {...props}
+      />
+    </div>
+  ));

+ 39 - 40
src/components/modules/EndpointModule/EndpointValidation/test.tsx

@@ -12,43 +12,42 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { shallow } from 'enzyme'
-import TW from '@src/utils/TestWrapper'
-import EndpointValidation from '.'
-
-const wrap = props => new TW(shallow(
-  
-  <EndpointValidation {...props} />
-), 'eValidation')
-
-describe('EndpointValidation Component', () => {
-  it('renders loading', () => {
-    let wrapper = wrap({ loading: true })
-    expect(wrapper.find('status').prop('loading')).toBe(true)
-    expect(wrapper.findText('title')).toBe('Validating Endpoint')
-  })
-
-  it('renders valid', () => {
-    let wrapper = wrap({ validation: { valid: true } })
-    expect(wrapper.find('status').prop('status')).toBe('COMPLETED')
-    expect(wrapper.findText('title')).toBe('Endpoint is Valid')
-  })
-
-  it('renders failed with default message', () => {
-    let wrapper = wrap({ validation: {} })
-    expect(wrapper.find('status').prop('status')).toBe('ERROR')
-    expect(wrapper.findText('title')).toBe('Validation Failed')
-    expect(wrapper.findText('errorMessage')).toBe('An unexpected error occurred.<CopyButton />')
-  })
-
-  it('renders failed with custom message', () => {
-    let wrapper = wrap({ validation: { message: 'custom_message' } })
-    expect(wrapper.find('status').prop('status')).toBe('ERROR')
-    expect(wrapper.findText('title')).toBe('Validation Failed')
-    expect(wrapper.findText('errorMessage')).toBe('custom_message<CopyButton />')
-  })
-})
-
-
-
+import React from "react";
+import { shallow } from "enzyme";
+import TW from "@src/utils/TestWrapper";
+import EndpointValidation from ".";
+
+const wrap = props =>
+  new TW(shallow(<EndpointValidation {...props} />), "eValidation");
+
+describe("EndpointValidation Component", () => {
+  it("renders loading", () => {
+    const wrapper = wrap({ loading: true });
+    expect(wrapper.find("status").prop("loading")).toBe(true);
+    expect(wrapper.findText("title")).toBe("Validating Endpoint");
+  });
+
+  it("renders valid", () => {
+    const wrapper = wrap({ validation: { valid: true } });
+    expect(wrapper.find("status").prop("status")).toBe("COMPLETED");
+    expect(wrapper.findText("title")).toBe("Endpoint is Valid");
+  });
+
+  it("renders failed with default message", () => {
+    const wrapper = wrap({ validation: {} });
+    expect(wrapper.find("status").prop("status")).toBe("ERROR");
+    expect(wrapper.findText("title")).toBe("Validation Failed");
+    expect(wrapper.findText("errorMessage")).toBe(
+      "An unexpected error occurred.<CopyButton />"
+    );
+  });
+
+  it("renders failed with custom message", () => {
+    const wrapper = wrap({ validation: { message: "custom_message" } });
+    expect(wrapper.find("status").prop("status")).toBe("ERROR");
+    expect(wrapper.findText("title")).toBe("Validation Failed");
+    expect(wrapper.findText("errorMessage")).toBe(
+      "custom_message<CopyButton />"
+    );
+  });
+});

+ 237 - 170
src/components/modules/LicenceModule/LicenceModule.tsx

@@ -12,48 +12,51 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import * as React from 'react'
-import { observer } from 'mobx-react'
-import styled, { css } from 'styled-components'
-import moment from 'moment'
+import * as React from "react";
+import { observer } from "mobx-react";
+import styled, { css } from "styled-components";
+import moment from "moment";
 
-import Button from '@src/components/ui/Button'
-import LoadingButton from '@src/components/ui/LoadingButton'
-import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
-import TextArea from '@src/components/ui/TextArea'
-import CopyValue from '@src/components/ui/CopyValue'
+import Button from "@src/components/ui/Button";
+import LoadingButton from "@src/components/ui/LoadingButton";
+import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
+import TextArea from "@src/components/ui/TextArea";
+import CopyValue from "@src/components/ui/CopyValue";
 
-import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import FileUtils from '@src/utils/FileUtils'
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
+import FileUtils from "@src/utils/FileUtils";
 
-import type { Licence, LicenceServerStatus } from '@src/@types/Licence'
+import type { Licence, LicenceServerStatus } from "@src/@types/Licence";
 
 // import OpenInNewIcon from '@src/components/ui/OpenInNewIcon'
-import OpenInNewIcon from '@src/components/ui/OpenInNewIcon'
-import { LEGAL_URLS } from '@src/constants'
-import licenceImage from './images/licence'
+import OpenInNewIcon from "@src/components/ui/OpenInNewIcon";
+import { LEGAL_URLS } from "@src/constants";
+import licenceImage from "./images/licence";
 
 const Wrapper = styled.div<any>`
   min-height: 0;
   overflow: auto;
   width: 100%;
-`
+`;
 const TextAreaStyled = styled(TextArea)`
-  ${props => (props.dropzone ? css`
-    border: 1px dashed ${ThemePalette.primary};
-  ` : '')}
-`
+  ${props =>
+    props.dropzone
+      ? css`
+          border: 1px dashed ${ThemePalette.primary};
+        `
+      : ""}
+`;
 const LicenceInfoWrapper = styled.div<any>`
   display: flex;
   flex-direction: column;
   box-sizing: border-box;
   width: 100%;
   padding: 0 32px;
-`
+`;
 const LicenceRow = styled.div<any>`
   display: flex;
   margin-top: 16px;
-`
+`;
 const LicenceRowLabel = styled.div<any>`
   display: flex;
   align-items: center;
@@ -61,47 +64,53 @@ const LicenceRowLabel = styled.div<any>`
   font-size: 10px;
   text-transform: uppercase;
   color: ${ThemePalette.grayscale[3]};
-`
+`;
 const LicenceLink = styled.span`
   text-transform: initial;
   color: ${ThemePalette.primary};
   font-weight: ${ThemeProps.fontWeights.regular};
   cursor: pointer;
-`
+`;
 const LicenceRowContent = styled.div<any>`
-  ${props => (props.width ? css`width: ${props.width};` : '')}
-`
+  ${props =>
+    props.width
+      ? css`
+          width: ${props.width};
+        `
+      : ""}
+`;
 const LicenceRowDescription = styled.div<any>`
   margin-top: 4px;
-`
+`;
 const LoadingWrapper = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
-`
+`;
 const ButtonsWrapper = styled.div<any>`
   display: flex;
   margin-top: 48px;
-  justify-content: ${props => (props.spaceBetween ? 'space-between' : 'center')};
+  justify-content: ${props =>
+    props.spaceBetween ? "space-between" : "center"};
   padding: 0 32px;
-`
+`;
 const Logo = styled.div<any>`
   width: 96px;
   height: 96px;
   margin: 0 auto;
   text-align: center;
-`
+`;
 const LicenceAddWrapper = styled.div<any>`
   padding: 0 32px;
-`
+`;
 const Description = styled.div<any>`
   color: ${ThemePalette.grayscale[3]};
-`
+`;
 const FakeFileInput = styled.input`
   position: absolute;
   opacity: 0;
   top: -99999px;
-`
+`;
 // const OutsideLinkLarge = styled.a`
 //   display: inline-block;
 //   color: ${ThemePalette.primary};
@@ -116,135 +125,146 @@ const OutsideLink = styled.a`
   text-decoration: none;
   margin-top: 8px;
   font-size: 12px;
-`
+`;
 const OpenInNewIconWrapper = styled.div`
-  ${ThemeProps.exactSize('16px')}
+  ${ThemeProps.exactSize("16px")}
   display: inline-block;
   position: relative;
   top: 9px;
   margin-top: -12px;
   transform: scale(0.6);
-`
+`;
 
 type Props = {
-  licenceInfo: Licence | null,
-  licenceServerStatus: LicenceServerStatus | null,
-  licenceError: string | null,
-  loadingLicenceInfo: boolean,
-  onRequestClose: () => void,
-  onAddModeChange: (addMode: boolean) => void,
-  addMode: boolean,
-  onAddLicence: (licence: string) => void,
-  addingLicence: boolean,
-  backButtonText: string
-}
+  licenceInfo: Licence | null;
+  licenceServerStatus: LicenceServerStatus | null;
+  licenceError: string | null;
+  loadingLicenceInfo: boolean;
+  onRequestClose: () => void;
+  onAddModeChange: (addMode: boolean) => void;
+  addMode: boolean;
+  onAddLicence: (licence: string) => void;
+  addingLicence: boolean;
+  backButtonText: string;
+};
 type State = {
-  licence: string,
-  isValid: boolean,
-  highlightDropzone: boolean,
-}
+  licence: string;
+  isValid: boolean;
+  highlightDropzone: boolean;
+};
 
 @observer
 class LicenceModule extends React.Component<Props, State> {
   state = {
-    licence: '',
+    licence: "",
     isValid: false,
     highlightDropzone: false,
-  }
+  };
 
-  fileInput!: HTMLElement
+  fileInput!: HTMLElement;
 
-  dragDropListeners: { type: string, listener: (e: any) => any }[] = []
+  dragDropListeners: { type: string; listener: (e: any) => any }[] = [];
 
   UNSAFE_componentWillReceiveProps(newProps: Props) {
     if (newProps.addMode === this.props.addMode) {
-      return
+      return;
     }
 
     if (newProps.addMode) {
-      setTimeout(() => { this.addDragAndDrop() }, 1000)
+      setTimeout(() => {
+        this.addDragAndDrop();
+      }, 1000);
     } else {
-      this.setState({ licence: '' })
-      this.removeDragDrop()
+      this.setState({ licence: "" });
+      this.removeDragDrop();
     }
   }
 
   addDragAndDrop() {
-    this.dragDropListeners = [{
-      type: 'dragenter',
-      listener: e => {
-        this.setState({ highlightDropzone: true })
-        e.dataTransfer.dropEffect = 'copy'
-        e.preventDefault()
+    this.dragDropListeners = [
+      {
+        type: "dragenter",
+        listener: e => {
+          this.setState({ highlightDropzone: true });
+          e.dataTransfer.dropEffect = "copy";
+          e.preventDefault();
+        },
       },
-    }, {
-      type: 'dragover',
-      listener: e => {
-        e.dataTransfer.dropEffect = 'copy'
-        e.preventDefault()
+      {
+        type: "dragover",
+        listener: e => {
+          e.dataTransfer.dropEffect = "copy";
+          e.preventDefault();
+        },
       },
-    }, {
-      type: 'dragleave',
-      listener: e => {
-        if (!e.clientX && !e.clientY) {
-          this.setState({ highlightDropzone: false })
-        }
+      {
+        type: "dragleave",
+        listener: e => {
+          if (!e.clientX && !e.clientY) {
+            this.setState({ highlightDropzone: false });
+          }
+        },
       },
-    }, {
-      type: 'drop',
-      listener: async e => {
-        e.preventDefault()
-        this.setState({ highlightDropzone: false })
-        const text = await FileUtils.readTextFromFirstFile(e.dataTransfer.files)
-        if (text) {
-          this.handleLicenceChange(text)
-        }
+      {
+        type: "drop",
+        listener: async e => {
+          e.preventDefault();
+          this.setState({ highlightDropzone: false });
+          const text = await FileUtils.readTextFromFirstFile(
+            e.dataTransfer.files
+          );
+          if (text) {
+            this.handleLicenceChange(text);
+          }
+        },
       },
-    }]
+    ];
 
     this.dragDropListeners.forEach(l => {
-      window.addEventListener(l.type, l.listener)
-    })
+      window.addEventListener(l.type, l.listener);
+    });
   }
 
   removeDragDrop() {
     this.dragDropListeners.forEach(l => {
-      window.removeEventListener(l.type, l.listener)
-    })
-    this.dragDropListeners = []
+      window.removeEventListener(l.type, l.listener);
+    });
+    this.dragDropListeners = [];
   }
 
   validate() {
-    let isValid = true
-    if (this.state.licence.indexOf('-----BEGIN CORIOLIS LICENCE-----') !== 0) {
-      isValid = false
+    let isValid = true;
+    if (this.state.licence.indexOf("-----BEGIN CORIOLIS LICENCE-----") !== 0) {
+      isValid = false;
     }
-    if (this.state.licence.indexOf('-----END CORIOLIS LICENCE-----') === -1) {
-      isValid = false
+    if (this.state.licence.indexOf("-----END CORIOLIS LICENCE-----") === -1) {
+      isValid = false;
     }
-    this.setState({ isValid })
+    this.setState({ isValid });
   }
 
   handleAddLicenceClick() {
-    this.props.onAddModeChange(true)
+    this.props.onAddModeChange(true);
   }
 
   handleAddButtonClick() {
-    this.props.onAddLicence(this.state.licence)
+    this.props.onAddLicence(this.state.licence);
   }
 
   handleLicenceChange(licence: string) {
-    this.setState({ licence }, () => { this.validate() })
+    this.setState({ licence }, () => {
+      this.validate();
+    });
   }
 
   handleUploadClick() {
-    this.fileInput.click()
+    this.fileInput.click();
   }
 
   async handleFileUpload(files: FileList) {
-    const text = await FileUtils.readTextFromFirstFile(files)
+    const text = await FileUtils.readTextFromFirstFile(files);
     if (text) {
-      this.handleLicenceChange(text)
+      this.handleLicenceChange(text);
     }
   }
 
@@ -253,37 +273,47 @@ class LicenceModule extends React.Component<Props, State> {
       <LoadingWrapper>
         <StatusImage loading />
       </LoadingWrapper>
-    )
+    );
   }
 
   renderExpiration(date: Date) {
-    const dateMoment = moment(date)
-    const days = dateMoment.diff(new Date(), 'days')
+    const dateMoment = moment(date);
+    const days = dateMoment.diff(new Date(), "days");
     if (days === 0) {
       return (
-        <span>today at <b>{dateMoment.utc().format('HH:mm')} UTC</b></span>
-      )
+        <span>
+          today at <b>{dateMoment.utc().format("HH:mm")} UTC</b>
+        </span>
+      );
     }
     return (
-      <span>on <b>{dateMoment.format('DD MMM YYYY')} ({days} days from now)</b></span>
-    )
+      <span>
+        on{" "}
+        <b>
+          {dateMoment.format("DD MMM YYYY")} ({days} days from now)
+        </b>
+      </span>
+    );
   }
 
   renderLicenceStatusText(info: Licence): React.ReactNode {
-    if (new Date(info.earliestLicenceExpiryDate).getTime() < new Date().getTime()) {
-      return 'Please contact Cloudbase Solutions with your Appliance ID in order to obtain a Coriolis® licence'
+    if (
+      new Date(info.earliestLicenceExpiryDate).getTime() < new Date().getTime()
+    ) {
+      return "Please contact Cloudbase Solutions with your Appliance ID in order to obtain a Coriolis® licence";
     }
     return (
       <LicenceRowDescription>
         Earliest Coriolis® Licence expires&nbsp;
         {this.renderExpiration(info.earliestLicenceExpiryDate)}.<br />
-        Latest Coriolis® Licence expires {this.renderExpiration(info.latestLicenceExpiryDate)}.
+        Latest Coriolis® Licence expires{" "}
+        {this.renderExpiration(info.latestLicenceExpiryDate)}.
       </LicenceRowDescription>
-    )
+    );
   }
 
   renderLicenceError(error: string) {
-    return <LicenceInfoWrapper>{error}</LicenceInfoWrapper>
+    return <LicenceInfoWrapper>{error}</LicenceInfoWrapper>;
   }
 
   renderLicenceInfo(info: Licence, status: LicenceServerStatus) {
@@ -294,9 +324,12 @@ class LicenceModule extends React.Component<Props, State> {
             <LicenceRowLabel>
               Licence
               <LicenceLink
-                style={{ marginLeft: '8px' }}
-                onClick={() => { this.handleAddLicenceClick() }}
-              >Add Licence
+                style={{ marginLeft: "8px" }}
+                onClick={() => {
+                  this.handleAddLicenceClick();
+                }}
+              >
+                Add Licence
               </LicenceLink>
             </LicenceRowLabel>
             {this.renderLicenceStatusText(info)}
@@ -306,18 +339,24 @@ class LicenceModule extends React.Component<Props, State> {
           <LicenceRowContent>
             <LicenceRowLabel>Appliance ID</LicenceRowLabel>
             <LicenceRowDescription>
-              <CopyValue value={`${info.applianceId}-licence${status.supported_licence_versions[0]}`} />
+              <CopyValue
+                value={`${info.applianceId}-licence${status.supported_licence_versions[0]}`}
+              />
             </LicenceRowDescription>
           </LicenceRowContent>
         </LicenceRow>
         <LicenceRow>
           <OutsideLink href={LEGAL_URLS.eula} target="_blank">
             Read Coriolis© EULA
-            <OpenInNewIconWrapper dangerouslySetInnerHTML={{ __html: OpenInNewIcon(ThemePalette.primary) }} />
+            <OpenInNewIconWrapper
+              dangerouslySetInnerHTML={{
+                __html: OpenInNewIcon(ThemePalette.primary),
+              }}
+            />
           </OutsideLink>
         </LicenceRow>
       </LicenceInfoWrapper>
-    )
+    );
   }
 
   renderButtons() {
@@ -327,97 +366,125 @@ class LicenceModule extends React.Component<Props, State> {
           <Button
             secondary
             large
-            onClick={() => { this.props.onAddModeChange(false) }}
-          >{this.props.backButtonText}
+            onClick={() => {
+              this.props.onAddModeChange(false);
+            }}
+          >
+            {this.props.backButtonText}
+          </Button>
+        ) : (
+          <Button
+            secondary
+            large
+            onClick={() => {
+              this.props.onRequestClose();
+            }}
+          >
+            Close
           </Button>
-        )
-          : (
-            <Button
-              secondary
-              large
-              onClick={() => { this.props.onRequestClose() }}
-            >Close
-            </Button>
-          )}
-        {(this.props.addMode && !this.props.addingLicence) ? (
+        )}
+        {this.props.addMode && !this.props.addingLicence ? (
           <Button
             large
-            onClick={() => { this.handleAddButtonClick() }}
+            onClick={() => {
+              this.handleAddButtonClick();
+            }}
             disabled={!this.state.isValid}
-          >Add Licence
+          >
+            Add Licence
           </Button>
-        )
-          : (this.props.addMode && this.props.addingLicence)
-            ? (
-              <LoadingButton
-                large
-                onClick={() => { this.handleAddButtonClick() }}
-              >Add Licence
-              </LoadingButton>
-            )
-            : null}
+        ) : this.props.addMode && this.props.addingLicence ? (
+          <LoadingButton
+            large
+            onClick={() => {
+              this.handleAddButtonClick();
+            }}
+          >
+            Add Licence
+          </LoadingButton>
+        ) : null}
       </ButtonsWrapper>
-    )
+    );
   }
 
   renderLicenceAdd() {
     const LicenceLinkComponent = (
       <LicenceLink
-        onClick={() => { this.handleUploadClick() }}
-      >upload
+        onClick={() => {
+          this.handleUploadClick();
+        }}
+      >
+        upload
       </LicenceLink>
-    )
+    );
 
     return (
       <LicenceAddWrapper>
         <Logo
-          dangerouslySetInnerHTML={
-            { __html: licenceImage(this.state.isValid ? ThemePalette.primary : ThemePalette.grayscale[5]) }
-          }
+          dangerouslySetInnerHTML={{
+            __html: licenceImage(
+              this.state.isValid
+                ? ThemePalette.primary
+                : ThemePalette.grayscale[5]
+            ),
+          }}
         />
-        <LicenceRowLabel style={{ marginTop: '32px' }}>Licence</LicenceRowLabel>
+        <LicenceRowLabel style={{ marginTop: "32px" }}>Licence</LicenceRowLabel>
         <TextAreaStyled
           placeholder="Paste/Drag Licence file here ..."
           dropzone={this.state.highlightDropzone}
           style={{
-            width: '100%',
-            marginTop: '4px',
+            width: "100%",
+            marginTop: "4px",
           }}
           value={this.state.licence}
-          onChange={(e: any) => { this.handleLicenceChange(e.target.value) }}
+          onChange={(e: any) => {
+            this.handleLicenceChange(e.target.value);
+          }}
         />
         <Description>
           Drag the Licence file or paste the contents in box above.
-          <br />Alternatively you can {LicenceLinkComponent} the file.
+          <br />
+          Alternatively you can {LicenceLinkComponent} the file.
         </Description>
         <FakeFileInput
           type="file"
-          ref={r => { this.fileInput = r as HTMLElement }}
+          ref={r => {
+            this.fileInput = r as HTMLElement;
+          }}
           accept=".pem, .txt"
-          onChange={(e: any) => { this.handleFileUpload(e.target.files) }}
+          onChange={(e: any) => {
+            this.handleFileUpload(e.target.files);
+          }}
         />
       </LicenceAddWrapper>
-    )
+    );
   }
 
   render() {
-    const showInfo = !this.props.loadingLicenceInfo
-      && !this.props.addMode && !this.props.licenceError
-    const showError = !this.props.loadingLicenceInfo && !this.props.addMode
+    const showInfo =
+      !this.props.loadingLicenceInfo &&
+      !this.props.addMode &&
+      !this.props.licenceError;
+    const showError = !this.props.loadingLicenceInfo && !this.props.addMode;
 
     return (
       <Wrapper>
-        {showInfo && this.props.licenceInfo
-          && this.props.licenceServerStatus
-          ? this.renderLicenceInfo(this.props.licenceInfo, this.props.licenceServerStatus) : null}
+        {showInfo && this.props.licenceInfo && this.props.licenceServerStatus
+          ? this.renderLicenceInfo(
+              this.props.licenceInfo,
+              this.props.licenceServerStatus
+            )
+          : null}
         {showError && this.props.licenceError
-          ? this.renderLicenceError(this.props.licenceError) : null}
+          ? this.renderLicenceError(this.props.licenceError)
+          : null}
         {this.props.addMode ? this.renderLicenceAdd() : null}
         {this.props.loadingLicenceInfo ? this.renderLicenceInfoLoading() : null}
         {this.renderButtons()}
       </Wrapper>
-    )
+    );
   }
 }
 
-export default LicenceModule
+export default LicenceModule;

+ 1 - 1
src/components/modules/LicenceModule/images/licence.ts

@@ -24,4 +24,4 @@ export default (color: string) => `
         </g>
     </g>
 </svg>
-`
+`;

+ 85 - 68
src/components/modules/LoginModule/LoginForm/LoginForm.tsx

@@ -12,31 +12,35 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React, { FormEvent } from 'react'
-import { observer } from 'mobx-react'
-import styled, { css } from 'styled-components'
+import React, { FormEvent } from "react";
+import { observer } from "mobx-react";
+import styled, { css } from "styled-components";
 
-import Button from '@src/components/ui/Button'
-import LoginOptions from '@src/components/modules/LoginModule/LoginOptions'
-import LoadingButton from '@src/components/ui/LoadingButton'
-import LoginFormField from '@src/components/modules/LoginModule/LoginFormField'
+import Button from "@src/components/ui/Button";
+import LoginOptions from "@src/components/modules/LoginModule/LoginOptions";
+import LoadingButton from "@src/components/ui/LoadingButton";
+import LoginFormField from "@src/components/modules/LoginModule/LoginFormField";
 
-import { loginButtons } from '@src/constants'
-import notificationStore from '@src/stores/NotificationStore'
-import { ThemeProps } from '@src/components/Theme'
-import errorIcon from './images/error.svg'
+import { loginButtons } from "@src/constants";
+import notificationStore from "@src/stores/NotificationStore";
+import { ThemeProps } from "@src/components/Theme";
+import errorIcon from "./images/error.svg";
 
 const Form = styled.form`
   background: rgba(221, 224, 229, 0.5);
   padding: 16px 32px 32px 32px;
   border-radius: 8px;
-`
+`;
 
 const FormFields = styled.div<any>`
   display: flex;
   margin-left: -16px;
-  ${loginButtons.length < 3 ? css`flex-direction: column;` : ''}
-`
+  ${loginButtons.length < 3
+    ? css`
+        flex-direction: column;
+      `
+    : ""}
+`;
 
 const LoginSeparator = styled.div<any>`
   margin: 8px 0 24px;
@@ -44,32 +48,32 @@ const LoginSeparator = styled.div<any>`
   display: flex;
   justify-content: center;
   align-items: center;
-`
+`;
 
 const SeparatorLine = styled.div<any>`
   width: 19px;
   border-top: 1px solid white;
-`
+`;
 
 const SeparatorText = styled.div<any>`
   font-size: 12px;
   color: white;
   flex-grow: 1;
   text-align: center;
-`
+`;
 const LoginError = styled.div<any>`
   display: flex;
   flex-direction: column;
   align-items: center;
   margin-top: 4px;
   margin-bottom: 16px;
-`
+`;
 
 const LoginErrorIcon = styled.div<any>`
   width: 26px;
   height: 26px;
-  background-image: url('${errorIcon}');
-`
+  background-image: url("${errorIcon}");
+`;
 
 const LoginErrorText = styled.div<any>`
   color: white;
@@ -77,85 +81,85 @@ const LoginErrorText = styled.div<any>`
   margin-top: 4px;
   width: ${ThemeProps.inputSizes.regular.width}px;
   text-align: center;
-`
+`;
 
 type Props = {
-  className: string,
-  showUserDomainInput: boolean,
-  loading: boolean,
-  loginFailedResponse: { status: string | number, message?: string },
-  domain: string,
-  onDomainChange: (domain: string) => void,
-  onFormSubmit: (credentials: {
-    username: string,
-    password: string,
-  }) => void,
-}
+  className: string;
+  showUserDomainInput: boolean;
+  loading: boolean;
+  loginFailedResponse: { status: string | number; message?: string };
+  domain: string;
+  onDomainChange: (domain: string) => void;
+  onFormSubmit: (credentials: { username: string; password: string }) => void;
+};
 type State = {
-  username: string,
-  password: string,
-}
+  username: string;
+  password: string;
+};
 @observer
 class LoginForm extends React.Component<Props, State> {
   static defaultProps = {
-    className: '',
-  }
+    className: "",
+  };
 
   state = {
-    username: '',
-    password: '',
-  }
+    username: "",
+    password: "",
+  };
 
   handleUsernameChange(username: string) {
-    this.setState({ username })
+    this.setState({ username });
   }
 
   handlePasswordChange(password: string) {
-    this.setState({ password })
+    this.setState({ password });
   }
 
   handleDomainChange(domain: string) {
-    this.props.onDomainChange(domain)
+    this.props.onDomainChange(domain);
   }
 
   handleFormSubmit(e: FormEvent) {
-    e.preventDefault()
-
-    if (!this.state.username.length || !this.state.password.length || !this.props.domain.length) {
-      notificationStore.alert('Please fill in all fields')
+    e.preventDefault();
+
+    if (
+      !this.state.username.length ||
+      !this.state.password.length ||
+      !this.props.domain.length
+    ) {
+      notificationStore.alert("Please fill in all fields");
     } else {
       this.props.onFormSubmit({
         username: this.state.username,
         password: this.state.password,
-      })
+      });
     }
   }
 
   renderErrorMessage() {
     if (!this.props.loginFailedResponse) {
-      return null
+      return null;
     }
 
-    let errorMessage = 'Request failed, there might be a problem with the connection to the server.'
+    let errorMessage =
+      "Request failed, there might be a problem with the connection to the server.";
 
     if (this.props.loginFailedResponse.status) {
       switch (this.props.loginFailedResponse.status) {
         case 401:
-          errorMessage = 'Incorrect credentials.<br />Please try again.'
-          break
+          errorMessage = "Incorrect credentials.<br />Please try again.";
+          break;
         default:
-          errorMessage = this.props.loginFailedResponse.message || errorMessage
+          errorMessage = this.props.loginFailedResponse.message || errorMessage;
       }
     }
 
     return (
       <LoginError>
         <LoginErrorIcon />
-        <LoginErrorText
-          dangerouslySetInnerHTML={{ __html: errorMessage }}
-        />
+        <LoginErrorText dangerouslySetInnerHTML={{ __html: errorMessage }} />
       </LoginError>
-    )
+    );
   }
 
   render() {
@@ -165,15 +169,22 @@ class LoginForm extends React.Component<Props, State> {
         <SeparatorText>or sign in with username</SeparatorText>
         <SeparatorLine />
       </LoginSeparator>
-    ) : null
+    ) : null;
 
-    const buttonStyle = { width: '100%', marginTop: '16px' }
-    const button = this.props.loading
-      ? <LoadingButton style={buttonStyle}>Please wait ... </LoadingButton>
-      : <Button style={buttonStyle}>Login</Button>
+    const buttonStyle = { width: "100%", marginTop: "16px" };
+    const button = this.props.loading ? (
+      <LoadingButton style={buttonStyle}>Please wait ... </LoadingButton>
+    ) : (
+      <Button style={buttonStyle}>Login</Button>
+    );
 
     return (
-      <Form className={this.props.className} onSubmit={e => { this.handleFormSubmit(e) }}>
+      <Form
+        className={this.props.className}
+        onSubmit={e => {
+          this.handleFormSubmit(e);
+        }}
+      >
         {this.renderErrorMessage()}
         <LoginOptions />
         {loginSeparator}
@@ -182,27 +193,33 @@ class LoginForm extends React.Component<Props, State> {
             <LoginFormField
               label="Domain Name"
               value={this.props.domain}
-              onChange={e => { this.handleDomainChange(e.target.value) }}
+              onChange={e => {
+                this.handleDomainChange(e.target.value);
+              }}
             />
           ) : null}
           <LoginFormField
             label="Username"
             value={this.state.username}
             name="username"
-            onChange={e => { this.handleUsernameChange(e.target.value) }}
+            onChange={e => {
+              this.handleUsernameChange(e.target.value);
+            }}
           />
           <LoginFormField
             label="Password"
             value={this.state.password}
-            onChange={e => { this.handlePasswordChange(e.target.value) }}
+            onChange={e => {
+              this.handlePasswordChange(e.target.value);
+            }}
             name="password"
             type="password"
           />
         </FormFields>
         {button}
       </Form>
-    )
+    );
   }
 }
 
-export default LoginForm
+export default LoginForm;

+ 1 - 2
src/components/modules/LoginModule/LoginForm/package.json

@@ -2,6 +2,5 @@
   "name": "LoginForm",
   "version": "0.0.0",
   "private": true,
-  "main":"./LoginForm.tsx"
+  "main": "./LoginForm.tsx"
 }
-

+ 9 - 15
src/components/modules/LoginModule/LoginForm/story.tsx

@@ -12,21 +12,15 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import LoginForm from '.'
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import LoginForm from ".";
 
-const props: any = {}
-storiesOf('LoginForm', module)
-  .add('default', () => (
-    <LoginForm {...props} />
-  ))
-  .add('loading', () => (
-    <LoginForm loading {...props} />
-  ))
-  .add('incorrect credentials', () => (
+const props: any = {};
+storiesOf("LoginForm", module)
+  .add("default", () => <LoginForm {...props} />)
+  .add("loading", () => <LoginForm loading {...props} />)
+  .add("incorrect credentials", () => (
     <LoginForm loginFailedResponse={{ status: 401 }} {...props} />
   ))
-  .add('server error', () => (
-    <LoginForm loginFailedResponse={{}} {...props} />
-  ))
+  .add("server error", () => <LoginForm loginFailedResponse={{}} {...props} />);

+ 40 - 35
src/components/modules/LoginModule/LoginForm/test.tsx

@@ -12,38 +12,43 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import React from 'react'
-import { shallow } from 'enzyme'
-import sinon from 'sinon'
-import TW from '@src/utils/TestWrapper'
-import LoginForm from '.'
-
-const wrap = props => new TW(shallow(
-
-  <LoginForm {...props} domain="default" />
-), 'loginForm')
-
-describe('LoginForm Component', () => {
-  it('renders incorrect credentials', () => {
-    let wrapper = wrap({ loginFailedResponse: { status: 401 } })
-    expect(wrapper.find('errorText').prop('dangerouslySetInnerHTML').__html).toBe('Incorrect credentials.<br />Please try again.') // eslint-disable-line
-  })
-
-  it('renders server error', () => {
-    let wrapper = wrap({ loginFailedResponse: {} })
-    expect(wrapper.find('errorText').prop('dangerouslySetInnerHTML').__html).toBe('Request failed, there might be a problem with the connection to the server.') // eslint-disable-line
-  })
-
-  it('submits correct info', () => {
-    let onFormSubmit = sinon.spy()
-    let wrapper = wrap({ onFormSubmit })
-    wrapper.find('usernameField').simulate('change', { target: { value: 'usr' } })
-    wrapper.find('passwordField').simulate('change', { target: { value: 'pswd' } })
-    wrapper.shallow.simulate('submit', { preventDefault: () => { } })
-    expect(onFormSubmit.args[0][0].username).toBe('usr')
-    expect(onFormSubmit.args[0][0].password).toBe('pswd')
-  })
-})
-
-
-
+import React from "react";
+import { shallow } from "enzyme";
+import sinon from "sinon";
+import TW from "@src/utils/TestWrapper";
+import LoginForm from ".";
+
+const wrap = props =>
+  new TW(shallow(<LoginForm {...props} domain="default" />), "loginForm");
+
+describe("LoginForm Component", () => {
+  it("renders incorrect credentials", () => {
+    const wrapper = wrap({ loginFailedResponse: { status: 401 } });
+    expect(
+      wrapper.find("errorText").prop("dangerouslySetInnerHTML").__html
+    ).toBe("Incorrect credentials.<br />Please try again."); // eslint-disable-line
+  });
+
+  it("renders server error", () => {
+    const wrapper = wrap({ loginFailedResponse: {} });
+    expect(
+      wrapper.find("errorText").prop("dangerouslySetInnerHTML").__html
+    ).toBe(
+      "Request failed, there might be a problem with the connection to the server."
+    ); // eslint-disable-line
+  });
+
+  it("submits correct info", () => {
+    const onFormSubmit = sinon.spy();
+    const wrapper = wrap({ onFormSubmit });
+    wrapper
+      .find("usernameField")
+      .simulate("change", { target: { value: "usr" } });
+    wrapper
+      .find("passwordField")
+      .simulate("change", { target: { value: "pswd" } });
+    wrapper.shallow.simulate("submit", { preventDefault: () => {} });
+    expect(onFormSubmit.args[0][0].username).toBe("usr");
+    expect(onFormSubmit.args[0][0].password).toBe("pswd");
+  });
+});

Vissa filer visades inte eftersom för många filer har ändrats