Răsfoiți Sursa

Merge pull request #707 from smiclea/bare-metal-servers

Add Coriolis Bare Metal Servers support
Daniel Vincze 3 ani în urmă
părinte
comite
8ef7e5296b
62 a modificat fișierele cu 2834 adăugiri și 338 ștergeri
  1. 1 0
      README.md
  2. 10 0
      config.ts
  3. 91 58
      server/api/resources/providerLogos/metal-32-white.svg
  4. 160 60
      server/api/resources/providerLogos/metal-32.svg
  5. 160 60
      server/api/resources/providerLogos/metal-42.svg
  6. 125 54
      server/api/resources/providerLogos/metal-64.svg
  7. 4 4
      server/main.ts
  8. 3 6
      server/proxies/azureProxy.ts
  9. 35 0
      server/proxies/metalHubProxy.ts
  10. 27 0
      server/proxies/router.ts
  11. 3 0
      src/@types/Config.ts
  12. 41 0
      src/@types/MetalHub.ts
  13. 4 0
      src/components/App.tsx
  14. 1 1
      src/components/modules/DetailsModule/DetailsContentHeader/DetailsContentHeader.tsx
  15. 77 0
      src/components/modules/MetalHubModule/MetalHubListHeader/MetalHubListHeader.tsx
  16. 6 0
      src/components/modules/MetalHubModule/MetalHubListHeader/package.json
  17. 133 0
      src/components/modules/MetalHubModule/MetalHubListItem/MetalHubListItem.tsx
  18. 92 0
      src/components/modules/MetalHubModule/MetalHubListItem/images/server.svg
  19. 6 0
      src/components/modules/MetalHubModule/MetalHubListItem/package.json
  20. 224 0
      src/components/modules/MetalHubModule/MetalHubModal/MetalHubModal.tsx
  21. 60 0
      src/components/modules/MetalHubModule/MetalHubModal/images/server.svg
  22. 6 0
      src/components/modules/MetalHubModule/MetalHubModal/package.json
  23. 376 0
      src/components/modules/MetalHubModule/MetalHubServerDetailsContent/MetalHubServerDetailsContent.tsx
  24. 6 0
      src/components/modules/MetalHubModule/MetalHubServerDetailsContent/package.json
  25. 5 1
      src/components/modules/NavigationModule/Navigation/Navigation.tsx
  26. 63 0
      src/components/modules/NavigationModule/Navigation/images/bare-metal-servers.svg
  27. 11 11
      src/components/modules/TransferModule/TransferDetailsTable/TransferDetailsTable.tsx
  28. 3 2
      src/components/modules/WizardModule/WizardOptions/WizardOptions.tsx
  29. 1 0
      src/components/modules/WizardModule/WizardPageContent/WizardPageContent.tsx
  30. 234 0
      src/components/smart/MetalHubServerDetailsPage/MetalHubServerDetailsPage.tsx
  31. 105 0
      src/components/smart/MetalHubServerDetailsPage/images/server.svg
  32. 6 0
      src/components/smart/MetalHubServerDetailsPage/package.json
  33. 195 0
      src/components/smart/MetalHubServersPage/MetalHubServersPage.tsx
  34. 75 0
      src/components/smart/MetalHubServersPage/images/server.svg
  35. 6 0
      src/components/smart/MetalHubServersPage/package.json
  36. 1 1
      src/components/smart/MinionPoolDetailsPage/MinionPoolDetailsPage.tsx
  37. 1 1
      src/components/smart/MinionPoolsPage/MinionPoolsPage.tsx
  38. 1 1
      src/components/smart/PageHeader/PageHeader.tsx
  39. 1 1
      src/components/smart/ReplicaDetailsPage/ReplicaDetailsPage.tsx
  40. 1 1
      src/components/smart/ReplicasPage/ReplicasPage.tsx
  41. 12 0
      src/components/smart/WizardPage/WizardPage.tsx
  42. 2 2
      src/components/ui/Dropdowns/ActionDropdown/ActionDropdown.spec.tsx
  43. 4 4
      src/components/ui/Dropdowns/ActionDropdown/ActionDropdown.tsx
  44. 6 2
      src/components/ui/Lists/FilterList/FilterList.tsx
  45. 5 0
      src/components/ui/Lists/MainList/MainList.tsx
  46. 2 2
      src/components/ui/Lists/MainListFilter/MainListFilter.spec.tsx
  47. 1 1
      src/components/ui/Lists/MainListFilter/MainListFilter.tsx
  48. 1 0
      src/constants.ts
  49. 79 57
      src/plugins/default/ContentPlugin.tsx
  50. 2 0
      src/plugins/index.ts
  51. 96 0
      src/plugins/metal/ContentPlugin.tsx
  52. 2 2
      src/sources/AzureSource.ts
  53. 4 0
      src/sources/InstanceSource.ts
  54. 94 0
      src/sources/MetalHubSource.ts
  55. 2 2
      src/stores/InstanceStore.ts
  56. 134 0
      src/stores/MetalHubStore.ts
  57. 4 0
      src/stores/ProviderStore.ts
  58. 5 2
      src/stores/WizardStore.ts
  59. 4 0
      src/utils/ApiCaller.ts
  60. 1 1
      src/utils/ApiCallerHandlers.ts
  61. 10 0
      src/utils/Cacher.ts
  62. 4 1
      webpack.dev.js

+ 1 - 0
README.md

@@ -58,4 +58,5 @@ The following is the list of environment variables and their default values:
 NODE_ENV='production'
 CORIOLIS_URL='<your-coriolis-url>'
 MOD_JSON='<path-to-json>'
+CA_FINGERPRINT='<path to CA fingerprint file used by metal hub agent>'
 ```

+ 10 - 0
config.ts

@@ -9,6 +9,7 @@ const conf: Config = {
     // Enabling users and projects page by default
     // 'users',
     // 'projects',
+    // 'bare-metal-servers',
   ],
 
   // Whether to show the user domain name input when logging in
@@ -38,6 +39,11 @@ const conf: Config = {
   // - `Infinity` value means no `limit` will be used, i.e. all VMs will be listed.
   instancesListBackgroundLoading: { default: 10, ovm: Infinity, 'hyper-v': Infinity },
 
+  /**
+   * The name of the Coriolis Bare Metal Hub enpoint used for doing Coriolis Bare Metal server operations.
+   */
+  bareMetalEndpointName: 'appliance-metal-hub',
+
   /**
    * The list of providers for which and extra source or destination options API call will be made,
    * if the required fields have any value set.
@@ -122,6 +128,9 @@ const conf: Config = {
     metal: 'Bare Metal',
   },
 
+  // The list of providers for which to disable setting the 'Execute Now Options' field
+  providersDisabledExecuteOptions: ['metal'],
+
   // The list of the users to hide in the UI
   hiddenUsers: ['barbican', 'coriolis'],
 
@@ -143,6 +152,7 @@ const conf: Config = {
     coriolisLogs: '{BASE_URL}/logs',
     coriolisLogStreamBaseUrl: '{BASE_URL}',
     coriolisLicensing: '{BASE_URL}/licensing',
+    metalhub: '{BASE_URL}/metal-hub',
     cloudbaseEmailEndpoint: 'http://localhost:3334',
   },
 }

+ 91 - 58
server/api/resources/providerLogos/metal-32-white.svg

@@ -4,110 +4,143 @@
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
-   width="185"
-   height="64"
+   id="svg1255"
+   version="1.1"
    viewBox="0 0 48.947916 16.933334"
-   version="1.1">
+   height="64"
+   width="185">
   <g
-     >
+     id="g1211">
     <text
-       xml:space="preserve"
-       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93889px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, @wght=400';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 400;letter-spacing:0px;fill:#ffffff;fill-opacity:1;stroke-width:0.151291;"
-       x="17.336496"
-       y="6.8236465"><tspan
-         y="6.8236465"
-         x="17.336496"
-         >Coriolis Bare</tspan><tspan
-         y="14.388196"
-         x="17.336496"
+       id="text1205"
+       y="-0.90159363"
+       x="16.080349"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.59809px;line-height:1;font-family:Arial;-inkscape-font-specification:Arial;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;fill:#ffffff;fill-opacity:1;stroke-width:0.171484"
+       xml:space="preserve"><tspan
+         style="stroke-width:0.171484"
+         y="7.6726017"
+         x="16.080349"
+         id="tspan1825"
+         ><tspan
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.171484"
+           id="tspan1203"
+           x="16.080349"
+           y="7.6726017">Coriolis Bare</tspan></tspan><tspan
+         style="stroke-width:0.171484"
+         y="13.270692"
+         x="16.080349"
+         id="tspan1827"
          >Metal Hub</tspan></text>
     <text
-       xml:space="preserve"
-       style="font-size:10.5833px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, Normal';stroke-width:0.264583"
+       id="text1209"
+       y="29.666916"
        x="29.132378"
-       y="29.666916"><tspan
-         x="29.132378"
+       style="font-size:10.5833px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, Normal';stroke-width:0.264583"
+       xml:space="preserve"><tspan
+         id="tspan1207"
+         style="stroke-width:0.264583"
          y="29.666916"
-         style="stroke-width:0.264583" /></text>
+         x="29.132378" /></text>
   </g>
   <g
+     id="g1253"
      transform="matrix(0.0292812,0,0,0.0292812,0.46207651,0.97066494)">
     <path
-       fill="#465a61"
+       id="path1213"
+       style="fill:#c7c7c7;fill-opacity:1"
        d="m 48.975,117.956 h 414.05 V 429.215 H 48.975 Z"
-       style="fill:#c7c7c7;fill-opacity:1" />
+       fill="#465a61" />
     <path
-       fill="#7a8c98"
+       id="path1215"
+       style="fill:#ffffff;fill-opacity:1"
        d="M 482.834,512 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 375.225 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 V 499.478 C 495.355,506.394 489.749,512 482.834,512 Z"
-       style="fill:#ffffff;fill-opacity:1" />
+       fill="#7a8c98" />
     <path
-       fill="#9facba"
+       id="path1217"
+       style="fill:#e8e8e8;fill-opacity:1"
        d="M 282.391,459.506 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
-       style="fill:#e8e8e8;fill-opacity:1" />
+       fill="#9facba" />
     <path
-       fill="#ffd15b"
+       id="path1219"
+       style="fill:#d5d5d5;fill-opacity:1"
        d="m 376.886,474.917 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.569 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
-       style="fill:#d5d5d5;fill-opacity:1" />
+       fill="#ffd15b" />
     <path
-       fill="#7a8c98"
+       id="path1221"
+       style="fill:#ffffff;fill-opacity:1"
        d="M 482.834,331.356 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 194.582 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
-       style="fill:#ffffff;fill-opacity:1" />
+       fill="#7a8c98" />
     <path
-       fill="#9facba"
+       id="path1223"
+       style="fill:#e8e8e8;fill-opacity:1"
        d="M 282.391,278.862 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
-       style="fill:#e8e8e8;fill-opacity:1" />
+       fill="#9facba" />
     <path
-       fill="#60b7ff"
+       id="path1225"
+       style="fill:#d5d5d5;fill-opacity:1"
        d="m 376.886,294.273 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.569 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
-       style="fill:#d5d5d5;fill-opacity:1" />
+       fill="#60b7ff" />
     <path
-       fill="#7a8c98"
+       id="path1227"
+       style="fill:#ffffff;fill-opacity:1"
        d="M 482.834,149.296 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 12.522 C 16.645,5.606 22.251,0 29.166,0 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
-       style="fill:#ffffff;fill-opacity:1" />
+       fill="#7a8c98" />
     <path
-       fill="#9facba"
+       id="path1229"
+       style="fill:#e8e8e8;fill-opacity:1"
        d="M 282.391,96.802 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
-       style="fill:#e8e8e8;fill-opacity:1" />
+       fill="#9facba" />
     <path
-       fill="#60b7ff"
+       id="path1231"
+       style="fill:#d5d5d5;fill-opacity:1"
        d="m 376.886,112.213 c -9.544,0 -17.28,-7.737 -17.28,-17.28 V 54.364 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
-       style="fill:#d5d5d5;fill-opacity:1" />
+       fill="#60b7ff" />
     <g
-       fill="#596c76"
-       style="fill:#dcdcdc;fill-opacity:1">
+       id="g1239"
+       style="fill:#dcdcdc;fill-opacity:1"
+       fill="#596c76">
       <path
-         d="m 482.834,362.704 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 375.225 c -10e-4,-6.915 -5.607,-12.521 -12.522,-12.521 z"
-         style="fill:#dcdcdc;fill-opacity:1" />
+         id="path1233"
+         style="fill:#dcdcdc;fill-opacity:1"
+         d="m 482.834,362.704 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 375.225 c -10e-4,-6.915 -5.607,-12.521 -12.522,-12.521 z" />
       <path
-         d="m 482.834,182.06 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 194.582 c -10e-4,-6.916 -5.607,-12.522 -12.522,-12.522 z"
-         style="fill:#dcdcdc;fill-opacity:1" />
+         id="path1235"
+         style="fill:#dcdcdc;fill-opacity:1"
+         d="m 482.834,182.06 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 194.582 c -10e-4,-6.916 -5.607,-12.522 -12.522,-12.522 z" />
       <path
-         d="m 482.834,0 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.916 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 12.522 C 495.355,5.606 489.749,0 482.834,0 Z"
-         style="fill:#dcdcdc;fill-opacity:1" />
+         id="path1237"
+         style="fill:#dcdcdc;fill-opacity:1"
+         d="m 482.834,0 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.916 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 12.522 C 495.355,5.606 489.749,0 482.834,0 Z" />
     </g>
     <path
-       fill="#ffd15b"
+       id="path1241"
+       style="fill:#d1d1d1;fill-opacity:1"
        d="m 445.745,37.083 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.022 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.57 c 0,-9.544 -7.737,-17.281 -17.28,-17.281 z"
-       style="fill:#d1d1d1;fill-opacity:1" />
+       fill="#ffd15b" />
     <path
-       fill="#ffc344"
+       id="path1243"
+       style="fill:#c7c7c7;fill-opacity:1"
        d="m 455.67,40.232 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 V 54.364 c 10e-4,-5.847 -2.91,-11.005 -7.354,-14.132 z"
-       style="fill:#c7c7c7;fill-opacity:1" />
+       fill="#ffc344" />
     <path
-       fill="#fe646f"
+       id="path1245"
+       style="fill:#d1d1d1;fill-opacity:1"
        d="m 445.745,219.143 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.569 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
-       style="fill:#d1d1d1;fill-opacity:1" />
+       fill="#fe646f" />
     <path
-       fill="#fd4755"
+       id="path1247"
+       style="fill:#c7c7c7;fill-opacity:1"
        d="m 455.67,222.292 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 v -40.569 c 10e-4,-5.846 -2.91,-11.004 -7.354,-14.131 z"
-       style="fill:#c7c7c7;fill-opacity:1" />
+       fill="#fd4755" />
     <path
-       fill="#60b7ff"
+       id="path1249"
+       style="fill:#d1d1d1;fill-opacity:1"
        d="m 445.745,399.787 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.569 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
-       style="fill:#d1d1d1;fill-opacity:1" />
+       fill="#60b7ff" />
     <path
-       fill="#26a6fe"
+       id="path1251"
+       style="fill:#c7c7c7;fill-opacity:1"
        d="m 455.67,402.936 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 v -40.569 c 10e-4,-5.846 -2.91,-11.004 -7.354,-14.131 z"
-       style="fill:#c7c7c7;fill-opacity:1" />
+       fill="#26a6fe" />
   </g>
 </svg>

+ 160 - 60
server/api/resources/providerLogos/metal-32.svg

@@ -4,113 +4,213 @@
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
-   width="185"
-   height="64"
+   id="svg964"
+   version="1.1"
    viewBox="0 0 48.947916 16.933334"
-   version="1.1">
+   height="64"
+   width="185">
   <g
-     >
+     id="g962">
     <g
+       id="g956"
        style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.05164px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, @wght=300';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 300;letter-spacing:0px;fill:#616870;fill-opacity:1;stroke-width:0.151291">
-      <text
-         y="6.9144459"
-         x="17.470165"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93889px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, @wght=300';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 300;stroke-width:0.0212755;"
-         xml:space="preserve"><tspan
-           y="6.9144459"
-           x="17.470165"
-           >Coriolis Bare</tspan><tspan
-           y="13.969995"
-           x="17.470165"
-           >Metal Hub</tspan></text>
       <g
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.45383px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.0234937"
+         id="text912"
+         aria-label="Coriolis Bare
+Metal Hub">
+        <path
+           id="path990"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="m 19.59244,5.6425524 0.516623,0.1304871 q -0.162444,0.6364577 -0.585861,0.9719961 -0.420754,0.3328753 -1.030582,0.3328753 -0.631132,0 -1.027919,-0.2556483 -0.394125,-0.2583112 -0.601839,-0.7456407 -0.205051,-0.4873296 -0.205051,-1.0465601 0,-0.6098277 0.231681,-1.0625382 0.234344,-0.4553734 0.663088,-0.6897177 0.431406,-0.2370072 0.948029,-0.2370072 0.585861,0 0.985311,0.2982563 0.39945,0.2982563 0.556568,0.8388459 L 19.533854,4.297736 Q 19.398041,3.8716555 19.13973,3.6772563 18.881418,3.4828571 18.489957,3.4828571 q -0.450048,0 -0.75363,0.2157032 -0.300919,0.2157033 -0.423417,0.5805347 -0.122498,0.3621683 -0.122498,0.7483038 0,0.4979815 0.143802,0.8708019 0.146465,0.3701574 0.45271,0.5539046 0.306245,0.1837472 0.663088,0.1837472 0.434069,0 0.734989,-0.2503223 0.300919,-0.2503223 0.407439,-0.7429778 z" />
+        <path
+           id="path992"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="m 20.508513,5.5972814 q 0,-0.7855859 0.436732,-1.1637323 0.364832,-0.3142343 0.889443,-0.3142343 0.583198,0 0.953355,0.3834724 0.370158,0.3808094 0.370158,1.0545491 0,0.5459156 -0.165106,0.8601499 -0.162444,0.3115714 -0.476678,0.4846665 -0.311571,0.1730952 -0.681729,0.1730952 -0.593849,0 -0.961344,-0.3808094 -0.364831,-0.3808094 -0.364831,-1.0971571 z m 0.492656,0 q 0,0.5432525 0.237007,0.8148788 0.237007,0.2689633 0.596512,0.2689633 0.356843,0 0.59385,-0.2716263 0.237007,-0.2716263 0.237007,-0.8281939 0,-0.5246115 -0.23967,-0.7935748 -0.237007,-0.2716263 -0.591187,-0.2716263 -0.359505,0 -0.596512,0.2689633 -0.237007,0.2689633 -0.237007,0.8122159 z" />
+        <path
+           id="path994"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="M 23.717431,7.0113358 V 4.1832269 h 0.431407 v 0.4287434 q 0.165106,-0.3009193 0.303582,-0.3967874 0.141139,-0.095868 0.308908,-0.095868 0.242333,0 0.492656,0.1544542 l -0.165106,0.4447214 q -0.175759,-0.1038571 -0.351517,-0.1038571 -0.157117,0 -0.282278,0.095868 -0.125161,0.093205 -0.178421,0.2609743 -0.07989,0.2556483 -0.07989,0.5592306 v 1.4806295 z" />
+        <path
+           id="path996"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="M 25.546914,3.6586153 V 3.1073737 h 0.479341 v 0.5512416 z m 0,3.3527205 V 4.1832269 h 0.479341 v 2.8281089 z" />
+        <path
+           id="path998"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="m 26.58016,5.5972814 q 0,-0.7855859 0.436732,-1.1637323 0.364832,-0.3142343 0.889443,-0.3142343 0.583198,0 0.953355,0.3834724 0.370158,0.3808094 0.370158,1.0545491 0,0.5459156 -0.165107,0.8601499 -0.162443,0.3115714 -0.476677,0.4846665 -0.311572,0.1730952 -0.681729,0.1730952 -0.59385,0 -0.961344,-0.3808094 Q 26.58016,6.3136291 26.58016,5.5972814 Z m 0.492655,0 q 0,0.5432525 0.237007,0.8148788 0.237008,0.2689633 0.596513,0.2689633 0.356842,0 0.59385,-0.2716263 0.237007,-0.2716263 0.237007,-0.8281939 0,-0.5246115 -0.23967,-0.7935748 -0.237008,-0.2716263 -0.591187,-0.2716263 -0.359505,0 -0.596513,0.2689633 -0.237007,0.2689633 -0.237007,0.8122159 z" />
+        <path
+           id="path1000"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="M 29.783752,7.0113358 V 3.1073737 h 0.47934 v 3.9039621 z" />
+        <path
+           id="path1002"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="M 31.011396,3.6586153 V 3.1073737 h 0.47934 v 0.5512416 z m 0,3.3527205 V 4.1832269 h 0.47934 v 2.8281089 z" />
+        <path
+           id="path1004"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="m 32.031326,6.167164 0.474015,-0.074564 q 0.03994,0.2849413 0.221029,0.4367324 0.183747,0.1517912 0.511296,0.1517912 0.330213,0 0.489993,-0.1331501 0.15978,-0.1358132 0.15978,-0.3168974 0,-0.1624431 -0.141139,-0.2556482 Q 33.647769,5.9115157 33.256307,5.8129846 32.729033,5.6798344 32.523982,5.5839663 32.321593,5.4854352 32.215073,5.3150031 32.111216,5.1419079 32.111216,4.9341937 q 0,-0.1890732 0.08522,-0.3488534 0.08788,-0.1624432 0.237007,-0.2689633 0.111847,-0.082553 0.303583,-0.1384761 0.194399,-0.058586 0.415428,-0.058586 0.332876,0 0.583198,0.095868 0.252985,0.095868 0.37282,0.2609743 0.119835,0.1624431 0.165106,0.4367324 l -0.468688,0.063912 Q 33.77293,4.7584355 33.618476,4.6359373 33.466685,4.5134392 33.187069,4.5134392 q -0.330212,0 -0.471351,0.1091831 -0.141139,0.1091831 -0.141139,0.2556483 0,0.093205 0.05859,0.1677692 0.05859,0.077227 0.183747,0.1278241 0.0719,0.02663 0.423417,0.1224981 0.508634,0.1358132 0.708359,0.2236923 0.202388,0.085216 0.316897,0.2503222 0.11451,0.1651062 0.11451,0.4101025 0,0.2396702 -0.14114,0.4527105 -0.138476,0.2103772 -0.402113,0.3275493 -0.263637,0.1145091 -0.596513,0.1145091 -0.551241,0 -0.841509,-0.2290182 Q 32.111216,6.6172114 32.031326,6.167164 Z" />
+        <path
+           id="path1006"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="M 36.502508,7.0113358 V 3.1073737 h 1.464652 q 0.447384,0 0.716348,0.1198351 0.271626,0.1171722 0.423417,0.3648314 0.154454,0.2449963 0.154454,0.5139596 0,0.2503222 -0.135813,0.4713515 -0.135813,0.2210292 -0.410102,0.3568424 0.354179,0.1038571 0.543252,0.3541793 0.191736,0.2503223 0.191736,0.5911867 0,0.2742892 -0.117172,0.5112965 -0.114509,0.2343442 -0.284941,0.3621684 -0.170432,0.1278241 -0.428743,0.1943992 -0.255649,0.063912 -0.628469,0.063912 z m 0.516623,-2.2635523 h 0.844172 q 0.343527,0 0.492655,-0.045271 0.197062,-0.058586 0.295594,-0.1943992 0.101194,-0.1358131 0.101194,-0.3408644 0,-0.1943992 -0.09321,-0.3408643 -0.09321,-0.1491282 -0.266301,-0.2023882 -0.173095,-0.055923 -0.593849,-0.055923 h -0.78026 z m 0,1.8028529 h 0.971996 q 0.250322,0 0.351516,-0.018641 0.178421,-0.031956 0.298257,-0.1065201 0.119835,-0.074564 0.197062,-0.2157032 0.07723,-0.1438021 0.07723,-0.3302123 0,-0.2183663 -0.111846,-0.3781464 Q 38.691497,5.3389701 38.491771,5.275058 38.294709,5.2084829 37.921889,5.2084829 h -0.902758 z" />
+        <path
+           id="path1008"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="m 41.951012,6.6624825 q -0.2663,0.2263552 -0.513959,0.3195603 -0.244996,0.093205 -0.527275,0.093205 -0.466025,0 -0.716347,-0.2263552 -0.250323,-0.2290183 -0.250323,-0.5831976 0,-0.2077143 0.0932,-0.3781464 0.09587,-0.1730952 0.24766,-0.2769523 0.154454,-0.1038571 0.34619,-0.1571172 0.141139,-0.037282 0.42608,-0.071901 0.580535,-0.069238 0.854824,-0.1651061 0.0027,-0.098531 0.0027,-0.1251612 0,-0.2929303 -0.135813,-0.4127654 -0.183747,-0.1624432 -0.545915,-0.1624432 -0.338202,0 -0.500645,0.1198351 -0.15978,0.1171722 -0.237007,0.4180915 l -0.468689,-0.063912 q 0.06391,-0.3009193 0.210378,-0.4846665 0.146465,-0.1864102 0.423417,-0.2849413 0.276952,-0.1011941 0.641784,-0.1011941 0.362168,0 0.588523,0.085216 0.226356,0.085216 0.332876,0.2157032 0.10652,0.1278241 0.149128,0.3248864 0.02397,0.1224981 0.02397,0.4420584 v 0.6391207 q 0,0.6684137 0.02929,0.8468349 0.03196,0.1757582 0.122498,0.3382013 H 42.04688 q -0.07456,-0.1491281 -0.09587,-0.3488533 z m -0.03994,-1.0705272 q -0.260974,0.1065202 -0.782922,0.1810842 -0.295594,0.042608 -0.418092,0.095868 -0.122498,0.05326 -0.189073,0.1571172 -0.06658,0.1011941 -0.06658,0.2263552 0,0.1917363 0.143802,0.3195604 0.146465,0.1278241 0.42608,0.1278241 0.276953,0 0.492656,-0.1198351 0.215703,-0.1224981 0.316897,-0.3328754 0.07723,-0.1624431 0.07723,-0.4793405 z" />
+        <path
+           id="path1010"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="M 43.136047,7.0113358 V 4.1832269 h 0.431407 v 0.4287434 q 0.165106,-0.3009193 0.303582,-0.3967874 0.141139,-0.095868 0.308908,-0.095868 0.242334,0 0.492656,0.1544542 l -0.165106,0.4447214 q -0.175758,-0.1038571 -0.351517,-0.1038571 -0.157117,0 -0.282278,0.095868 -0.125161,0.093205 -0.178421,0.2609743 -0.07989,0.2556483 -0.07989,0.5592306 v 1.4806295 z" />
+        <path
+           id="path1012"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0234937"
+           d="m 46.898871,6.1005889 0.495319,0.061249 q -0.117172,0.4340694 -0.434069,0.6737397 -0.316898,0.2396702 -0.809553,0.2396702 -0.62048,0 -0.985311,-0.3808094 -0.362169,-0.3834724 -0.362169,-1.0731901 0,-0.7136848 0.367495,-1.1078092 0.367494,-0.3941244 0.953355,-0.3941244 0.567219,0 0.926725,0.3861354 0.359505,0.3861354 0.359505,1.0865051 0,0.042608 -0.0027,0.1278242 h -2.109098 q 0.02663,0.4660255 0.263637,0.7136847 0.237007,0.2476593 0.591187,0.2476593 0.263637,0 0.450047,-0.1384761 0.18641,-0.1384762 0.295593,-0.4420585 z M 45.325037,5.3256551 h 1.57916 Q 46.872241,4.9688127 46.723113,4.7903915 46.494095,4.5134392 46.129264,4.5134392 q -0.330213,0 -0.556568,0.2210292 -0.223692,0.2210293 -0.247659,0.5911867 z" />
+        <path
+           id="path1014"
+           style="stroke-width:0.0234937"
+           d="M 16.790961,13.828623 V 9.9246609 h 0.777597 l 0.924062,2.7641971 q 0.127824,0.386135 0.18641,0.577871 0.06658,-0.21304 0.207714,-0.625805 l 0.934714,-2.7162631 h 0.695044 v 3.9039621 h -0.497981 v -3.267504 l -1.13444,3.267504 h -0.466025 l -1.129113,-3.323427 v 3.323427 z" />
+        <path
+           id="path1016"
+           style="stroke-width:0.0234937"
+           d="m 23.230102,12.917876 0.495319,0.06125 q -0.117172,0.43407 -0.43407,0.67374 -0.316897,0.23967 -0.809553,0.23967 -0.620479,0 -0.985311,-0.380809 -0.362168,-0.383473 -0.362168,-1.07319 0,-0.713685 0.367494,-1.10781 0.367495,-0.394124 0.953355,-0.394124 0.56722,0 0.926725,0.386135 0.359506,0.386136 0.359506,1.086506 0,0.04261 -0.0027,0.127824 h -2.109099 q 0.02663,0.466025 0.263638,0.713684 0.237007,0.24766 0.591186,0.24766 0.263638,0 0.450048,-0.138476 0.18641,-0.138477 0.295593,-0.442059 z m -1.573835,-0.774934 h 1.579161 q -0.03196,-0.356842 -0.181084,-0.535263 -0.229018,-0.276953 -0.59385,-0.276953 -0.330212,0 -0.556567,0.22103 -0.223693,0.221029 -0.24766,0.591186 z" />
+        <path
+           id="path1018"
+           style="stroke-width:0.0234937"
+           d="m 25.376483,13.39988 0.06924,0.423417 q -0.202389,0.04261 -0.362169,0.04261 -0.260974,0 -0.404776,-0.08255 -0.143802,-0.08255 -0.202388,-0.215703 -0.05859,-0.135813 -0.05859,-0.56722 v -1.627095 h -0.351516 v -0.37282 h 0.351516 v -0.70037 l 0.476678,-0.287604 v 0.987974 h 0.482004 v 0.37282 h -0.482004 v 1.653725 q 0,0.205051 0.02397,0.263637 0.02663,0.05859 0.08255,0.09321 0.05859,0.03462 0.165106,0.03462 0.07989,0 0.210378,-0.01864 z" />
+        <path
+           id="path1020"
+           style="stroke-width:0.0234937"
+           d="m 27.687969,13.47977 q -0.2663,0.226355 -0.51396,0.31956 -0.244996,0.09321 -0.527274,0.09321 -0.466026,0 -0.716348,-0.226355 -0.250322,-0.229018 -0.250322,-0.583198 0,-0.207714 0.09321,-0.378146 0.09587,-0.173095 0.247659,-0.276952 0.154454,-0.103858 0.346191,-0.157118 0.141139,-0.03728 0.42608,-0.0719 0.580535,-0.06924 0.854824,-0.165106 0.0027,-0.09853 0.0027,-0.125161 0,-0.29293 -0.135813,-0.412765 -0.183747,-0.162444 -0.545916,-0.162444 -0.338201,0 -0.500644,0.119836 -0.15978,0.117172 -0.237008,0.418091 l -0.468688,-0.06391 q 0.06391,-0.300919 0.210377,-0.484667 0.146465,-0.18641 0.423418,-0.284941 0.276952,-0.101194 0.641783,-0.101194 0.362169,0 0.588524,0.08522 0.226355,0.08522 0.332875,0.215703 0.10652,0.127824 0.149128,0.324887 0.02397,0.122498 0.02397,0.442058 v 0.639121 q 0,0.668414 0.02929,0.846835 0.03196,0.175758 0.122499,0.338201 h -0.500645 q -0.07456,-0.149128 -0.09587,-0.348853 z m -0.03994,-1.070527 q -0.260974,0.10652 -0.782923,0.181084 -0.295593,0.04261 -0.418091,0.09587 -0.122498,0.05326 -0.189074,0.157117 -0.06658,0.101194 -0.06658,0.226355 0,0.191736 0.143803,0.319561 0.146465,0.127824 0.42608,0.127824 0.276952,0 0.492656,-0.119835 0.215703,-0.122499 0.316897,-0.332876 0.07723,-0.162443 0.07723,-0.47934 z" />
+        <path
+           id="path1022"
+           style="stroke-width:0.0234937"
+           d="M 28.867679,13.828623 V 9.9246609 h 0.47934 v 3.9039621 z" />
+        <path
+           id="path1024"
+           style="stroke-width:0.0234937"
+           d="M 31.682473,13.828623 V 9.9246609 h 0.516623 v 1.6031281 h 2.029208 V 9.9246609 h 0.516623 v 3.9039621 h -0.516623 v -1.840135 h -2.029208 v 1.840135 z" />
+        <path
+           id="path1026"
+           style="stroke-width:0.0234937"
+           d="m 37.39994,13.828623 v -0.415428 q -0.330212,0.47934 -0.897432,0.47934 -0.250322,0 -0.468688,-0.09587 -0.215703,-0.09587 -0.322223,-0.23967 -0.103858,-0.146465 -0.146466,-0.356843 -0.02929,-0.141139 -0.02929,-0.447384 v -1.752256 h 0.479341 v 1.568509 q 0,0.375483 0.02929,0.50597 0.04527,0.189073 0.191736,0.298257 0.146465,0.10652 0.362169,0.10652 0.215703,0 0.404776,-0.109183 0.189073,-0.111847 0.2663,-0.30092 0.07989,-0.191736 0.07989,-0.553904 v -1.515249 h 0.479341 v 2.828109 z" />
+        <path
+           id="path1028"
+           style="stroke-width:0.0234937"
+           d="M 39.024371,13.828623 H 38.579649 V 9.9246609 h 0.479341 v 1.3927501 q 0.303582,-0.380809 0.774934,-0.380809 0.260974,0 0.492655,0.10652 0.234344,0.103857 0.383473,0.295593 0.151791,0.189074 0.237007,0.458037 0.08522,0.268963 0.08522,0.575209 0,0.726999 -0.359505,1.123787 -0.359506,0.396787 -0.862813,0.396787 -0.500645,0 -0.785586,-0.418091 z m -0.0053,-1.435358 q 0,0.508633 0.138476,0.734988 0.226355,0.370158 0.612491,0.370158 0.314234,0 0.543252,-0.271627 0.229018,-0.274289 0.229018,-0.814878 0,-0.553905 -0.221029,-0.817542 -0.218366,-0.263638 -0.529937,-0.263638 -0.314235,0 -0.543253,0.27429 -0.229018,0.271626 -0.229018,0.788249 z" />
+      </g>
+      <g
+         id="g954"
          transform="matrix(0.02954208,0,0,0.02954208,0.30493538,0.90387952)">
         <path
-           fill="#465a61"
+           id="path914"
+           style="fill:#7f8890;fill-opacity:1"
            d="m 48.975,117.956 h 414.05 V 429.215 H 48.975 Z"
-           style="fill:#7f8890;fill-opacity:1" />
+           fill="#465a61" />
         <path
-           fill="#7a8c98"
+           id="path916"
+           style="fill:#616870;fill-opacity:1"
            d="M 482.834,512 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 375.225 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 V 499.478 C 495.355,506.394 489.749,512 482.834,512 Z"
-           style="fill:#616870;fill-opacity:1" />
+           fill="#7a8c98" />
         <path
-           fill="#9facba"
+           id="path918"
+           style="fill:#828a93;fill-opacity:1"
            d="M 282.391,459.506 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#9facba" />
         <path
-           fill="#ffd15b"
+           id="path920"
+           style="fill:#747d87;fill-opacity:1"
            d="m 376.886,474.917 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.569 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
-           style="fill:#747d87;fill-opacity:1" />
+           fill="#ffd15b" />
         <path
-           fill="#7a8c98"
+           id="path922"
+           style="fill:#616870;fill-opacity:1"
            d="M 482.834,331.356 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 194.582 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
-           style="fill:#616870;fill-opacity:1" />
+           fill="#7a8c98" />
         <path
-           fill="#9facba"
+           id="path924"
+           style="fill:#828a93;fill-opacity:1"
            d="M 282.391,278.862 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#9facba" />
         <path
-           fill="#60b7ff"
+           id="path926"
+           style="fill:#747d87;fill-opacity:1"
            d="m 376.886,294.273 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.569 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
-           style="fill:#747d87;fill-opacity:1" />
+           fill="#60b7ff" />
         <path
-           fill="#7a8c98"
+           id="path928"
+           style="fill:#616870;fill-opacity:1"
            d="M 482.834,149.296 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 12.522 C 16.645,5.606 22.251,0 29.166,0 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
-           style="fill:#616870;fill-opacity:1" />
+           fill="#7a8c98" />
         <path
-           fill="#9facba"
+           id="path930"
+           style="fill:#828a93;fill-opacity:1"
            d="M 282.391,96.802 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#9facba" />
         <path
-           fill="#60b7ff"
+           id="path932"
+           style="fill:#747d87;fill-opacity:1"
            d="m 376.886,112.213 c -9.544,0 -17.28,-7.737 -17.28,-17.28 V 54.364 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
-           style="fill:#747d87;fill-opacity:1" />
+           fill="#60b7ff" />
         <g
-           fill="#596c76"
-           style="fill:#555b62;fill-opacity:1">
+           id="g940"
+           style="fill:#555b62;fill-opacity:1"
+           fill="#596c76">
           <path
-             d="m 482.834,362.704 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 375.225 c -10e-4,-6.915 -5.607,-12.521 -12.522,-12.521 z"
-             style="fill:#555b62;fill-opacity:1" />
+             id="path934"
+             style="fill:#555b62;fill-opacity:1"
+             d="m 482.834,362.704 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 375.225 c -10e-4,-6.915 -5.607,-12.521 -12.522,-12.521 z" />
           <path
-             d="m 482.834,182.06 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 194.582 c -10e-4,-6.916 -5.607,-12.522 -12.522,-12.522 z"
-             style="fill:#555b62;fill-opacity:1" />
+             id="path936"
+             style="fill:#555b62;fill-opacity:1"
+             d="m 482.834,182.06 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 194.582 c -10e-4,-6.916 -5.607,-12.522 -12.522,-12.522 z" />
           <path
-             d="m 482.834,0 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.916 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 12.522 C 495.355,5.606 489.749,0 482.834,0 Z"
-             style="fill:#555b62;fill-opacity:1" />
+             id="path938"
+             style="fill:#555b62;fill-opacity:1"
+             d="m 482.834,0 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.916 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 12.522 C 495.355,5.606 489.749,0 482.834,0 Z" />
         </g>
         <path
-           fill="#ffd15b"
+           id="path942"
+           style="fill:#828a93;fill-opacity:1"
            d="m 445.745,37.083 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.022 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.57 c 0,-9.544 -7.737,-17.281 -17.28,-17.281 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#ffd15b" />
         <path
-           fill="#ffc344"
+           id="path944"
+           style="fill:#6b737b;fill-opacity:1"
            d="m 455.67,40.232 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 V 54.364 c 10e-4,-5.847 -2.91,-11.005 -7.354,-14.132 z"
-           style="fill:#6b737b;fill-opacity:1" />
+           fill="#ffc344" />
         <path
-           fill="#fe646f"
+           id="path946"
+           style="fill:#828a93;fill-opacity:1"
            d="m 445.745,219.143 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.569 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#fe646f" />
         <path
-           fill="#fd4755"
+           id="path948"
+           style="fill:#6b737b;fill-opacity:1"
            d="m 455.67,222.292 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 v -40.569 c 10e-4,-5.846 -2.91,-11.004 -7.354,-14.131 z"
-           style="fill:#6b737b;fill-opacity:1" />
+           fill="#fd4755" />
         <path
-           fill="#60b7ff"
+           id="path950"
+           style="fill:#828a93;fill-opacity:1"
            d="m 445.745,399.787 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.569 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#60b7ff" />
         <path
-           fill="#26a6fe"
+           id="path952"
+           style="fill:#6b737b;fill-opacity:1"
            d="m 455.67,402.936 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 v -40.569 c 10e-4,-5.846 -2.91,-11.004 -7.354,-14.131 z"
-           style="fill:#6b737b;fill-opacity:1" />
+           fill="#26a6fe" />
       </g>
     </g>
     <text
-       xml:space="preserve"
-       style="font-size:10.5833px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, Normal';stroke-width:0.264583"
+       id="text960"
+       y="29.666916"
        x="29.132378"
-       y="29.666916"><tspan
-         x="29.132378"
+       style="font-size:10.5833px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, Normal';stroke-width:0.264583"
+       xml:space="preserve"><tspan
+         id="tspan958"
+         style="stroke-width:0.264583"
          y="29.666916"
-         style="stroke-width:0.264583" /></text>
+         x="29.132378" /></text>
   </g>
 </svg>

+ 160 - 60
server/api/resources/providerLogos/metal-42.svg

@@ -4,113 +4,213 @@
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
-   width="185"
-   height="64"
+   id="svg2027"
+   version="1.1"
    viewBox="0 0 48.947916 16.933334"
-   version="1.1">
+   height="64"
+   width="185">
   <g
-     >
+     id="g2025">
     <g
+       id="g2019"
        style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.05164px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, @wght=300';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 300;letter-spacing:0px;fill:#616870;fill-opacity:1;stroke-width:0.151291">
-      <text
-         y="6.9144459"
-         x="17.470165"
-         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93889px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, @wght=300';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 300;stroke-width:0.0212755;"
-         xml:space="preserve"><tspan
-           y="6.9144459"
-           x="17.470165"
-           >Coriolis Bare</tspan><tspan
-           y="13.969995"
-           x="17.470165"
-           >Metal Hub</tspan></text>
       <g
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.5063px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.0237198"
+         id="text1975"
+         aria-label="Coriolis Bare
+Metal Hub">
+        <path
+           id="path2607"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="m 19.737997,5.6153822 0.521593,0.1317426 q -0.164006,0.6425809 -0.591498,0.9813474 -0.424802,0.3360779 -1.040497,0.3360779 -0.637203,0 -1.037808,-0.2581079 -0.397916,-0.2607964 -0.607629,-0.7528144 -0.207024,-0.492018 -0.207024,-1.0566289 0,-0.6156946 0.23391,-1.0727605 0.236599,-0.4597546 0.669467,-0.6963534 0.435557,-0.2392875 0.95715,-0.2392875 0.591497,0 0.994791,0.3011258 0.403293,0.3011258 0.561922,0.8469163 l -0.513527,0.120988 Q 19.541727,3.8274479 19.280931,3.6311784 19.020134,3.434909 18.624907,3.434909 q -0.454378,0 -0.760881,0.2177784 -0.303814,0.2177785 -0.427491,0.5861198 -0.123676,0.3656528 -0.123676,0.7555031 0,0.5027725 0.145185,0.8791798 0.147875,0.3737186 0.457066,0.5592335 0.309192,0.185515 0.669467,0.185515 0.438246,0 0.74206,-0.2527305 0.303815,-0.2527306 0.41136,-0.7501259 z" />
+        <path
+           id="path2609"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="m 20.662883,5.5696756 q 0,-0.7931437 0.440934,-1.1749282 0.368341,-0.3172575 0.898,-0.3172575 0.588809,0 0.962527,0.3871617 0.373719,0.3844731 0.373719,1.0646947 0,0.5511677 -0.166695,0.8684252 -0.164006,0.3145689 -0.481263,0.4893294 -0.314569,0.1747605 -0.688288,0.1747605 -0.599563,0 -0.970593,-0.3844731 -0.368341,-0.3844731 -0.368341,-1.1077127 z m 0.497395,0 q 0,0.5484791 0.239288,0.8227187 0.239287,0.2715509 0.602251,0.2715509 0.360276,0 0.599563,-0.2742395 0.239288,-0.2742396 0.239288,-0.8361618 0,-0.5296587 -0.241977,-0.8012097 -0.239287,-0.2742395 -0.596874,-0.2742395 -0.362964,0 -0.602251,0.2715509 -0.239288,0.271551 -0.239288,0.82003 z" />
+        <path
+           id="path2611"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="M 23.902674,6.9973345 V 4.1420168 h 0.435557 v 0.4328683 q 0.166694,-0.3038144 0.306503,-0.4006048 0.142497,-0.09679 0.31188,-0.09679 0.244665,0 0.497395,0.1559401 l -0.166694,0.449 q -0.17745,-0.1048562 -0.354899,-0.1048562 -0.158628,0 -0.284994,0.09679 -0.126365,0.094102 -0.180137,0.263485 -0.08066,0.2581078 -0.08066,0.5646109 v 1.4948744 z" />
+        <path
+           id="path2613"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="m 25.749757,3.6123581 v -0.556545 h 0.483953 v 0.556545 z m 0,3.3849764 V 4.1420168 h 0.483953 v 2.8553177 z" />
+        <path
+           id="path2615"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="m 26.792943,5.5696756 q 0,-0.7931437 0.440935,-1.1749282 0.368341,-0.3172575 0.898,-0.3172575 0.588808,0 0.962527,0.3871617 0.373718,0.3844731 0.373718,1.0646947 0,0.5511677 -0.166694,0.8684252 -0.164006,0.3145689 -0.481264,0.4893294 -0.314569,0.1747605 -0.688287,0.1747605 -0.599563,0 -0.970593,-0.3844731 -0.368342,-0.3844731 -0.368342,-1.1077127 z m 0.497396,0 q 0,0.5484791 0.239287,0.8227187 0.239287,0.2715509 0.602252,0.2715509 0.360275,0 0.599563,-0.2742395 0.239287,-0.2742396 0.239287,-0.8361618 0,-0.5296587 -0.241976,-0.8012097 -0.239288,-0.2742395 -0.596874,-0.2742395 -0.362965,0 -0.602252,0.2715509 -0.239287,0.271551 -0.239287,0.82003 z" />
+        <path
+           id="path2617"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="M 30.027357,6.9973345 V 3.0558131 h 0.483952 v 3.9415214 z" />
+        <path
+           id="path2619"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="m 31.266812,3.6123581 v -0.556545 h 0.483952 v 0.556545 z m 0,3.3849764 V 4.1420168 h 0.483952 v 2.8553177 z" />
+        <path
+           id="path2621"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="m 32.296555,6.145041 0.478575,-0.075282 q 0.04033,0.2876827 0.223155,0.4409342 0.185515,0.1532515 0.516216,0.1532515 0.333389,0 0.494707,-0.1344311 0.161317,-0.1371198 0.161317,-0.3199462 0,-0.164006 -0.142497,-0.2581078 -0.09948,-0.064527 -0.494707,-0.164006 Q 33.000974,5.653023 32.79395,5.5562325 32.589615,5.4567535 32.48207,5.2846816 32.377213,5.1099211 32.377213,4.9002085 q 0,-0.1908922 0.08604,-0.3522096 0.08873,-0.164006 0.239288,-0.2715509 0.112922,-0.083347 0.306503,-0.1398084 0.196269,-0.05915 0.419425,-0.05915 0.336078,0 0.588809,0.09679 0.255419,0.09679 0.376407,0.2634851 0.120988,0.164006 0.166694,0.4409341 l -0.473197,0.064527 Q 34.054914,4.7227594 33.898974,4.5990827 33.745723,4.4754061 33.463417,4.4754061 q -0.333389,0 -0.475886,0.1102335 -0.142497,0.1102336 -0.142497,0.2581078 0,0.094102 0.05915,0.1693833 0.05915,0.07797 0.185515,0.1290539 0.07259,0.026886 0.427491,0.1236766 0.513527,0.1371198 0.715173,0.2258444 0.204336,0.086036 0.319946,0.2527305 0.115611,0.1666947 0.115611,0.414048 0,0.2419761 -0.142497,0.4570659 -0.139808,0.2124012 -0.405982,0.3307006 -0.266173,0.1156108 -0.602251,0.1156108 -0.556545,0 -0.849605,-0.2312215 -0.290372,-0.2312216 -0.37103,-0.6855989 z" />
+        <path
+           id="path2623"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="M 36.810753,6.9973345 V 3.0558131 h 1.478742 q 0.451689,0 0.72324,0.120988 0.27424,0.1182995 0.427491,0.3683414 0.15594,0.2473533 0.15594,0.5189043 0,0.2527305 -0.13712,0.4758862 -0.137119,0.2231557 -0.414048,0.3602755 0.357587,0.1048563 0.54848,0.3575869 0.19358,0.2527305 0.19358,0.5968743 0,0.2769282 -0.118299,0.5162156 -0.115611,0.2365988 -0.287683,0.3656527 -0.172072,0.129054 -0.432868,0.1962695 -0.258108,0.064527 -0.634515,0.064527 z m 0.521593,-2.2853296 h 0.852293 q 0.346832,0 0.497395,-0.045707 0.198958,-0.05915 0.298438,-0.1962695 0.102167,-0.1371197 0.102167,-0.3441437 0,-0.1962695 -0.0941,-0.3441438 -0.0941,-0.1505629 -0.268862,-0.2043353 -0.17476,-0.056461 -0.599563,-0.056461 h -0.787766 z m 0,1.8201978 h 0.981347 q 0.252731,0 0.354898,-0.01882 0.180138,-0.032263 0.301126,-0.1075449 0.120988,-0.075281 0.198958,-0.2177785 0.07797,-0.1451856 0.07797,-0.3333892 0,-0.2204671 -0.112922,-0.3817845 -0.112922,-0.164006 -0.314569,-0.2285329 -0.198958,-0.067216 -0.575365,-0.067216 h -0.911443 z" />
+        <path
+           id="path2625"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="m 42.311675,6.6451249 q -0.268862,0.2285329 -0.518904,0.3226347 -0.247354,0.094102 -0.532348,0.094102 -0.470509,0 -0.723239,-0.2285329 -0.252731,-0.2312216 -0.252731,-0.5888085 0,-0.2097126 0.0941,-0.3817844 0.09679,-0.1747605 0.250042,-0.2796168 0.15594,-0.1048563 0.349521,-0.1586288 0.142497,-0.037641 0.43018,-0.072593 0.586119,-0.069904 0.863048,-0.1666946 0.0027,-0.099479 0.0027,-0.1263653 0,-0.2957486 -0.13712,-0.4167366 -0.185515,-0.164006 -0.551167,-0.164006 -0.341455,0 -0.505461,0.120988 -0.161318,0.1182994 -0.239288,0.4221139 L 40.3678,4.9566696 q 0.06453,-0.3038144 0.212402,-0.4893294 0.147874,-0.1882036 0.427491,-0.2876827 0.279616,-0.1021676 0.647958,-0.1021676 0.365653,0 0.594186,0.086036 0.228532,0.086036 0.336077,0.2177785 0.107545,0.1290539 0.150563,0.328012 0.0242,0.1236766 0.0242,0.4463114 v 0.6452695 q 0,0.6748444 0.02958,0.8549822 0.03226,0.1774491 0.123676,0.3414551 h -0.505461 q -0.07528,-0.1505629 -0.09679,-0.3522096 z m -0.04033,-1.0808265 q -0.263486,0.1075449 -0.790456,0.1828264 -0.298437,0.043018 -0.422113,0.09679 -0.123677,0.053772 -0.190893,0.1586288 -0.06721,0.1021676 -0.06721,0.2285329 0,0.1935809 0.145185,0.3226348 0.147875,0.1290539 0.43018,0.1290539 0.279617,0 0.497395,-0.1209881 0.217779,-0.1236766 0.319946,-0.3360778 0.07797,-0.164006 0.07797,-0.4839522 z" />
+        <path
+           id="path2627"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="M 43.508113,6.9973345 V 4.1420168 h 0.435557 v 0.4328683 q 0.166694,-0.3038144 0.306503,-0.4006048 0.142497,-0.09679 0.31188,-0.09679 0.244665,0 0.497395,0.1559401 l -0.166694,0.449 q -0.177449,-0.1048562 -0.354898,-0.1048562 -0.158629,0 -0.284995,0.09679 -0.126365,0.094102 -0.180137,0.263485 -0.08066,0.2581078 -0.08066,0.5646109 v 1.4948744 z" />
+        <path
+           id="path2629"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;stroke-width:0.0237198"
+           d="m 47.307137,6.0778254 0.500084,0.061838 q -0.1183,0.4382456 -0.438246,0.6802217 -0.319946,0.241976 -0.817341,0.241976 -0.626449,0 -0.994791,-0.3844731 -0.365652,-0.3871617 -0.365652,-1.083515 0,-0.720551 0.37103,-1.1184672 0.37103,-0.3979162 0.962527,-0.3979162 0.572676,0 0.93564,0.3898503 0.362965,0.3898503 0.362965,1.0969582 0,0.043018 -0.0027,0.1290539 h -2.129389 q 0.02689,0.470509 0.266173,0.720551 0.239288,0.2500419 0.596875,0.2500419 0.266173,0 0.454377,-0.1398084 0.188203,-0.1398084 0.298437,-0.4463114 z M 45.718161,5.2954361 h 1.594353 Q 47.280251,4.9351606 47.129688,4.7550229 46.898466,4.4754061 46.530125,4.4754061 q -0.333389,0 -0.561922,0.2231557 -0.225845,0.2231557 -0.250042,0.5968743 z" />
+        <path
+           id="path2631"
+           style="stroke-width:0.0237198"
+           d="M 16.909565,13.880209 V 9.9386881 h 0.785078 l 0.932952,2.7907909 q 0.129054,0.38985 0.188204,0.583431 0.06722,-0.21509 0.209713,-0.631826 l 0.943706,-2.7423959 h 0.701731 v 3.9415209 h -0.502773 v -3.29894 l -1.145353,3.29894 h -0.470509 l -1.139976,-3.355401 v 3.355401 z" />
+        <path
+           id="path2633"
+           style="stroke-width:0.0237198"
+           d="m 23.410656,12.9607 0.500084,0.06184 q -0.1183,0.438245 -0.438246,0.680221 -0.319946,0.241976 -0.817341,0.241976 -0.626449,0 -0.994791,-0.384473 -0.365652,-0.387161 -0.365652,-1.083515 0,-0.720551 0.37103,-1.118467 0.37103,-0.397916 0.962527,-0.397916 0.572676,0 0.93564,0.38985 0.362965,0.389851 0.362965,1.096958 0,0.04302 -0.0027,0.129054 h -2.12939 q 0.02689,0.470509 0.266174,0.720551 0.239288,0.250042 0.596874,0.250042 0.266174,0 0.454378,-0.139808 0.188203,-0.139809 0.298437,-0.446312 z M 21.82168,12.178311 h 1.594353 q -0.03226,-0.360275 -0.182826,-0.540413 -0.231222,-0.279617 -0.599563,-0.279617 -0.333389,0 -0.561922,0.223156 -0.225845,0.223155 -0.250042,0.596874 z" />
+        <path
+           id="path2635"
+           style="stroke-width:0.0237198"
+           d="m 25.577686,13.447341 0.0699,0.427491 q -0.204336,0.04302 -0.365653,0.04302 -0.263485,0 -0.408671,-0.08335 -0.145185,-0.08335 -0.204335,-0.217779 -0.05915,-0.137119 -0.05915,-0.572676 v -1.642749 h -0.354898 v -0.376407 h 0.354898 v -0.707108 l 0.481264,-0.290371 v 0.997479 h 0.48664 v 0.376407 h -0.48664 v 1.669635 q 0,0.207024 0.0242,0.266174 0.02689,0.05915 0.08335,0.0941 0.05915,0.03495 0.166694,0.03495 0.08066,0 0.212401,-0.01882 z" />
+        <path
+           id="path2637"
+           style="stroke-width:0.0237198"
+           d="m 27.911411,13.528 q -0.268862,0.228533 -0.518904,0.322635 -0.247353,0.0941 -0.532348,0.0941 -0.470509,0 -0.723239,-0.228533 -0.252731,-0.231221 -0.252731,-0.588808 0,-0.209713 0.0941,-0.381784 0.09679,-0.174761 0.250042,-0.279617 0.15594,-0.104857 0.349521,-0.158629 0.142497,-0.03764 0.43018,-0.07259 0.58612,-0.0699 0.863048,-0.166694 0.0027,-0.09948 0.0027,-0.126366 0,-0.295748 -0.137119,-0.416736 -0.185515,-0.164006 -0.551168,-0.164006 -0.341455,0 -0.505461,0.120988 -0.161318,0.118299 -0.239288,0.422114 l -0.473197,-0.06453 q 0.06453,-0.303815 0.212401,-0.48933 0.147874,-0.188203 0.427491,-0.287682 0.279617,-0.102168 0.647958,-0.102168 0.365653,0 0.594186,0.08604 0.228533,0.08604 0.336078,0.217778 0.107545,0.129054 0.150563,0.328012 0.0242,0.123677 0.0242,0.446312 v 0.645269 q 0,0.674845 0.02958,0.854982 0.03226,0.177449 0.123677,0.341455 H 28.008202 Q 27.93292,13.729647 27.911411,13.528 Z m -0.04033,-1.080827 q -0.263485,0.107545 -0.790455,0.182827 -0.298438,0.04302 -0.422114,0.09679 -0.123677,0.05377 -0.190892,0.158629 -0.06722,0.102168 -0.06722,0.228533 0,0.193581 0.145186,0.322635 0.147874,0.129054 0.430179,0.129054 0.279617,0 0.497396,-0.120988 0.217778,-0.123677 0.319946,-0.336078 0.07797,-0.164006 0.07797,-0.483953 z" />
+        <path
+           id="path2639"
+           style="stroke-width:0.0237198"
+           d="M 29.102471,13.880209 V 9.9386881 h 0.483952 v 3.9415209 z" />
+        <path
+           id="path2641"
+           style="stroke-width:0.0237198"
+           d="M 31.944345,13.880209 V 9.9386881 h 0.521593 v 1.6185509 h 2.048731 V 9.9386881 h 0.521593 v 3.9415209 h -0.521593 v -1.857838 h -2.048731 v 1.857838 z" />
+        <path
+           id="path2643"
+           style="stroke-width:0.0237198"
+           d="m 37.716818,13.880209 v -0.419425 q -0.333389,0.483952 -0.906066,0.483952 -0.25273,0 -0.473197,-0.09679 -0.217779,-0.09679 -0.325324,-0.241976 -0.104856,-0.147874 -0.147874,-0.360276 -0.02958,-0.142497 -0.02958,-0.451688 v -1.769114 h 0.483952 v 1.583599 q 0,0.379096 0.02958,0.510838 0.04571,0.190892 0.193581,0.301126 0.147874,0.107545 0.365653,0.107545 0.217778,0 0.40867,-0.110234 0.190893,-0.112922 0.268863,-0.303814 0.08066,-0.193581 0.08066,-0.559234 v -1.529826 h 0.483953 v 2.855317 z" />
+        <path
+           id="path2645"
+           style="stroke-width:0.0237198"
+           d="m 39.356879,13.880209 h -0.449 V 9.9386881 h 0.483952 v 1.4061499 q 0.306503,-0.384473 0.78239,-0.384473 0.263485,0 0.497395,0.107545 0.236599,0.104856 0.387162,0.298437 0.153251,0.190892 0.239287,0.462443 0.08604,0.271551 0.08604,0.580743 0,0.733994 -0.362964,1.134599 -0.362964,0.400604 -0.871114,0.400604 -0.505461,0 -0.793144,-0.422113 z m -0.0054,-1.449167 q 0,0.513527 0.139808,0.74206 0.228533,0.373718 0.618384,0.373718 0.317257,0 0.548479,-0.274239 0.231221,-0.276929 0.231221,-0.822719 0,-0.559234 -0.223155,-0.825407 -0.220468,-0.266174 -0.535036,-0.266174 -0.317258,0 -0.54848,0.276928 -0.231221,0.27424 -0.231221,0.795833 z" />
+      </g>
+      <g
+         id="g2017"
          transform="matrix(0.02954208,0,0,0.02954208,0.30493538,0.90387952)">
         <path
-           fill="#465a61"
+           id="path1977"
+           style="fill:#7f8890;fill-opacity:1"
            d="m 48.975,117.956 h 414.05 V 429.215 H 48.975 Z"
-           style="fill:#7f8890;fill-opacity:1" />
+           fill="#465a61" />
         <path
-           fill="#7a8c98"
+           id="path1979"
+           style="fill:#616870;fill-opacity:1"
            d="M 482.834,512 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 375.225 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 V 499.478 C 495.355,506.394 489.749,512 482.834,512 Z"
-           style="fill:#616870;fill-opacity:1" />
+           fill="#7a8c98" />
         <path
-           fill="#9facba"
+           id="path1981"
+           style="fill:#828a93;fill-opacity:1"
            d="M 282.391,459.506 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#9facba" />
         <path
-           fill="#ffd15b"
+           id="path1983"
+           style="fill:#747d87;fill-opacity:1"
            d="m 376.886,474.917 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.569 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
-           style="fill:#747d87;fill-opacity:1" />
+           fill="#ffd15b" />
         <path
-           fill="#7a8c98"
+           id="path1985"
+           style="fill:#616870;fill-opacity:1"
            d="M 482.834,331.356 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 194.582 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
-           style="fill:#616870;fill-opacity:1" />
+           fill="#7a8c98" />
         <path
-           fill="#9facba"
+           id="path1987"
+           style="fill:#828a93;fill-opacity:1"
            d="M 282.391,278.862 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#9facba" />
         <path
-           fill="#60b7ff"
+           id="path1989"
+           style="fill:#747d87;fill-opacity:1"
            d="m 376.886,294.273 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.569 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
-           style="fill:#747d87;fill-opacity:1" />
+           fill="#60b7ff" />
         <path
-           fill="#7a8c98"
+           id="path1991"
+           style="fill:#616870;fill-opacity:1"
            d="M 482.834,149.296 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 12.522 C 16.645,5.606 22.251,0 29.166,0 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
-           style="fill:#616870;fill-opacity:1" />
+           fill="#7a8c98" />
         <path
-           fill="#9facba"
+           id="path1993"
+           style="fill:#828a93;fill-opacity:1"
            d="M 282.391,96.802 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#9facba" />
         <path
-           fill="#60b7ff"
+           id="path1995"
+           style="fill:#747d87;fill-opacity:1"
            d="m 376.886,112.213 c -9.544,0 -17.28,-7.737 -17.28,-17.28 V 54.364 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
-           style="fill:#747d87;fill-opacity:1" />
+           fill="#60b7ff" />
         <g
-           fill="#596c76"
-           style="fill:#555b62;fill-opacity:1">
+           id="g2003"
+           style="fill:#555b62;fill-opacity:1"
+           fill="#596c76">
           <path
-             d="m 482.834,362.704 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 375.225 c -10e-4,-6.915 -5.607,-12.521 -12.522,-12.521 z"
-             style="fill:#555b62;fill-opacity:1" />
+             id="path1997"
+             style="fill:#555b62;fill-opacity:1"
+             d="m 482.834,362.704 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 375.225 c -10e-4,-6.915 -5.607,-12.521 -12.522,-12.521 z" />
           <path
-             d="m 482.834,182.06 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 194.582 c -10e-4,-6.916 -5.607,-12.522 -12.522,-12.522 z"
-             style="fill:#555b62;fill-opacity:1" />
+             id="path1999"
+             style="fill:#555b62;fill-opacity:1"
+             d="m 482.834,182.06 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 194.582 c -10e-4,-6.916 -5.607,-12.522 -12.522,-12.522 z" />
           <path
-             d="m 482.834,0 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.916 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 12.522 C 495.355,5.606 489.749,0 482.834,0 Z"
-             style="fill:#555b62;fill-opacity:1" />
+             id="path2001"
+             style="fill:#555b62;fill-opacity:1"
+             d="m 482.834,0 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.916 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 12.522 C 495.355,5.606 489.749,0 482.834,0 Z" />
         </g>
         <path
-           fill="#ffd15b"
+           id="path2005"
+           style="fill:#828a93;fill-opacity:1"
            d="m 445.745,37.083 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.022 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.57 c 0,-9.544 -7.737,-17.281 -17.28,-17.281 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#ffd15b" />
         <path
-           fill="#ffc344"
+           id="path2007"
+           style="fill:#6b737b;fill-opacity:1"
            d="m 455.67,40.232 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 V 54.364 c 10e-4,-5.847 -2.91,-11.005 -7.354,-14.132 z"
-           style="fill:#6b737b;fill-opacity:1" />
+           fill="#ffc344" />
         <path
-           fill="#fe646f"
+           id="path2009"
+           style="fill:#828a93;fill-opacity:1"
            d="m 445.745,219.143 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.569 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#fe646f" />
         <path
-           fill="#fd4755"
+           id="path2011"
+           style="fill:#6b737b;fill-opacity:1"
            d="m 455.67,222.292 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 v -40.569 c 10e-4,-5.846 -2.91,-11.004 -7.354,-14.131 z"
-           style="fill:#6b737b;fill-opacity:1" />
+           fill="#fd4755" />
         <path
-           fill="#60b7ff"
+           id="path2013"
+           style="fill:#828a93;fill-opacity:1"
            d="m 445.745,399.787 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.569 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
-           style="fill:#828a93;fill-opacity:1" />
+           fill="#60b7ff" />
         <path
-           fill="#26a6fe"
+           id="path2015"
+           style="fill:#6b737b;fill-opacity:1"
            d="m 455.67,402.936 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 v -40.569 c 10e-4,-5.846 -2.91,-11.004 -7.354,-14.131 z"
-           style="fill:#6b737b;fill-opacity:1" />
+           fill="#26a6fe" />
       </g>
     </g>
     <text
-       xml:space="preserve"
-       style="font-size:10.5833px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, Normal';stroke-width:0.264583"
+       id="text2023"
+       y="29.666916"
        x="29.132378"
-       y="29.666916"><tspan
-         x="29.132378"
+       style="font-size:10.5833px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, Normal';stroke-width:0.264583"
+       xml:space="preserve"><tspan
+         id="tspan2021"
+         style="stroke-width:0.264583"
          y="29.666916"
-         style="stroke-width:0.264583" /></text>
+         x="29.132378" /></text>
   </g>
 </svg>

+ 125 - 54
server/api/resources/providerLogos/metal-64.svg

@@ -4,91 +4,162 @@
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
-   width="185"
-   height="64"
-   viewBox="0 0 48.947916 16.933334"
+   id="svg2702"
    version="1.1"
-   >
+   viewBox="0 0 48.947916 16.933334"
+   height="64"
+   width="185">
   <g
-     >
-    <text
-       xml:space="preserve"
+     transform="matrix(1.0466902,0,0,1.0466902,-2.2389037,-0.63842709)"
+     id="g2658">
+    <g
        style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.2676px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, @wght=400';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 400;letter-spacing:0px;stroke-width:0.13169"
-       x="17.404707"
-       y="7.0365133"><tspan
-         y="7.0365133"
-         x="17.404707"
-         >Coriolis Bare</tspan><tspan
-         y="13.621014"
-         x="17.404707"
-         >Metal Hub</tspan></text>
-    <text
-       xml:space="preserve"
+       id="text2652"
+       aria-label="Coriolis BareMetal Hub">
+      <path
+         id="path3271"
+         d="m 19.190423,7.0891893 q -0.484619,0 -0.805942,-0.1790984 -0.316056,-0.1790984 -0.479352,-0.500422 -0.163296,-0.3213236 -0.179098,-0.7479992 -0.0053,-0.2159716 -0.0053,-0.4635488 0,-0.2475772 0.0053,-0.474084 0.0158,-0.4266756 0.179098,-0.7479992 0.163296,-0.3213236 0.479352,-0.500422 0.321323,-0.1790984 0.805942,-0.1790984 0.363465,0 0.63738,0.1000844 0.273915,0.1000844 0.453014,0.2686476 0.184366,0.1685632 0.279182,0.3792672 0.09482,0.2054364 0.105352,0.4266756 0.0053,0.047408 -0.03161,0.079014 -0.03161,0.031606 -0.07901,0.031606 h -0.289718 q -0.04741,0 -0.08428,-0.026338 -0.0316,-0.026338 -0.04741,-0.094817 -0.08955,-0.4108728 -0.337127,-0.5636332 -0.242309,-0.1527604 -0.611041,-0.1527604 -0.421408,0 -0.668985,0.2423096 -0.247578,0.237042 -0.268648,0.763802 -0.0158,0.4319432 0,0.8849568 0.02107,0.52676 0.268648,0.7690696 0.247577,0.237042 0.668985,0.237042 0.368732,0 0.611041,-0.1527604 0.247578,-0.1527604 0.337127,-0.5636332 0.0158,-0.068479 0.04741,-0.094817 0.03687,-0.026338 0.08428,-0.026338 h 0.289718 q 0.04741,0 0.07901,0.031606 0.03687,0.031606 0.03161,0.079014 -0.01054,0.2212392 -0.105352,0.4319432 -0.09482,0.2054364 -0.279182,0.3739996 -0.179099,0.1685632 -0.453014,0.2686476 -0.273915,0.1000844 -0.63738,0.1000844 z" />
+      <path
+         id="path3273"
+         d="m 22.424812,7.0891893 q -0.400338,0 -0.668985,-0.1527604 -0.268648,-0.1527604 -0.410873,-0.421408 -0.142225,-0.2739152 -0.158028,-0.6215768 -0.0053,-0.089549 -0.0053,-0.2265068 0,-0.1422252 0.0053,-0.2265068 0.0158,-0.3529292 0.158028,-0.6215768 0.147493,-0.2686476 0.41614,-0.421408 0.268648,-0.1527604 0.663718,-0.1527604 0.39507,0 0.663718,0.1527604 0.268647,0.1527604 0.410872,0.421408 0.147493,0.2686476 0.163296,0.6215768 0.0053,0.084282 0.0053,0.2265068 0,0.1369576 -0.0053,0.2265068 -0.0158,0.3476616 -0.158028,0.6215768 -0.142225,0.2686476 -0.410873,0.421408 -0.268647,0.1527604 -0.668985,0.1527604 z m 0,-0.4056052 q 0.326591,0 0.521492,-0.2054364 0.194902,-0.210704 0.210704,-0.6110416 0.0053,-0.079014 0.0053,-0.2001688 0,-0.1211548 -0.0053,-0.2001688 -0.0158,-0.4003376 -0.210704,-0.605774 -0.194901,-0.210704 -0.521492,-0.210704 -0.326591,0 -0.52676,0.210704 -0.194901,0.2054364 -0.205436,0.605774 -0.0053,0.079014 -0.0053,0.2001688 0,0.1211548 0.0053,0.2001688 0.01053,0.4003376 0.205436,0.6110416 0.200169,0.2054364 0.52676,0.2054364 z" />
+      <path
+         id="path3275"
+         d="m 24.448723,7.0365133 q -0.05268,0 -0.08955,-0.031606 -0.03161,-0.036873 -0.03161,-0.089549 V 4.4237837 q 0,-0.052676 0.03161,-0.089549 0.03687,-0.036873 0.08955,-0.036873 h 0.242309 q 0.05268,0 0.08955,0.036873 0.03687,0.036873 0.03687,0.089549 v 0.2317744 q 0.105352,-0.1790984 0.289718,-0.2686476 0.184366,-0.089549 0.442478,-0.089549 h 0.210704 q 0.05268,0 0.08428,0.036873 0.03161,0.031606 0.03161,0.084282 v 0.2159716 q 0,0.052676 -0.03161,0.084282 -0.03161,0.031606 -0.08428,0.031606 h -0.316056 q -0.28445,0 -0.447746,0.1685632 -0.163295,0.1632956 -0.163295,0.447746 v 1.5486744 q 0,0.052676 -0.03687,0.089549 -0.03687,0.031606 -0.08955,0.031606 z" />
+      <path
+         id="path3277"
+         d="m 26.454937,7.0365133 q -0.05268,0 -0.08955,-0.031606 -0.0316,-0.036873 -0.0316,-0.089549 V 4.4185161 q 0,-0.052676 0.0316,-0.084282 0.03687,-0.036873 0.08955,-0.036873 h 0.252845 q 0.05268,0 0.08428,0.036873 0.0316,0.031606 0.0316,0.084282 v 2.4968424 q 0,0.052676 -0.0316,0.089549 -0.03161,0.031606 -0.08428,0.031606 z m -0.04214,-3.2501092 q -0.05268,0 -0.08955,-0.031606 -0.0316,-0.036873 -0.0316,-0.089549 V 3.3807989 q 0,-0.052676 0.0316,-0.084282 0.03687,-0.036873 0.08955,-0.036873 h 0.331858 q 0.05268,0 0.08428,0.036873 0.03687,0.031606 0.03687,0.084282 v 0.2844504 q 0,0.052676 -0.03687,0.089549 -0.03161,0.031606 -0.08428,0.031606 z" />
+      <path
+         id="path3279"
+         d="m 28.741817,7.0891893 q -0.400338,0 -0.668985,-0.1527604 -0.268648,-0.1527604 -0.410873,-0.421408 -0.142225,-0.2739152 -0.158028,-0.6215768 -0.0053,-0.089549 -0.0053,-0.2265068 0,-0.1422252 0.0053,-0.2265068 0.0158,-0.3529292 0.158028,-0.6215768 0.147493,-0.2686476 0.41614,-0.421408 0.268648,-0.1527604 0.663718,-0.1527604 0.39507,0 0.663718,0.1527604 0.268647,0.1527604 0.410872,0.421408 0.147493,0.2686476 0.163296,0.6215768 0.0053,0.084282 0.0053,0.2265068 0,0.1369576 -0.0053,0.2265068 -0.0158,0.3476616 -0.158028,0.6215768 -0.142225,0.2686476 -0.410873,0.421408 -0.268647,0.1527604 -0.668985,0.1527604 z m 0,-0.4056052 q 0.326591,0 0.521492,-0.2054364 0.194902,-0.210704 0.210704,-0.6110416 0.0053,-0.079014 0.0053,-0.2001688 0,-0.1211548 -0.0053,-0.2001688 -0.0158,-0.4003376 -0.210704,-0.605774 -0.194901,-0.210704 -0.521492,-0.210704 -0.326591,0 -0.52676,0.210704 -0.194901,0.2054364 -0.205436,0.605774 -0.0053,0.079014 -0.0053,0.2001688 0,0.1211548 0.0053,0.2001688 0.01054,0.4003376 0.205436,0.6110416 0.200169,0.2054364 0.52676,0.2054364 z" />
+      <path
+         id="path3281"
+         d="m 30.770995,7.0365133 q -0.05268,0 -0.08955,-0.031606 -0.03161,-0.036873 -0.03161,-0.089549 V 3.4176721 q 0,-0.052676 0.03161,-0.084282 0.03687,-0.036873 0.08955,-0.036873 h 0.247577 q 0.05794,0 0.08955,0.036873 0.03161,0.031606 0.03161,0.084282 v 3.4976864 q 0,0.052676 -0.03161,0.089549 -0.0316,0.031606 -0.08955,0.031606 z" />
+      <path
+         id="path3283"
+         d="m 32.092916,7.0365133 q -0.05268,0 -0.08955,-0.031606 -0.03161,-0.036873 -0.03161,-0.089549 V 4.4185161 q 0,-0.052676 0.03161,-0.084282 0.03687,-0.036873 0.08955,-0.036873 h 0.252845 q 0.05268,0 0.08428,0.036873 0.03161,0.031606 0.03161,0.084282 v 2.4968424 q 0,0.052676 -0.03161,0.089549 -0.0316,0.031606 -0.08428,0.031606 z m -0.04214,-3.2501092 q -0.05268,0 -0.08955,-0.031606 -0.03161,-0.036873 -0.03161,-0.089549 V 3.3807989 q 0,-0.052676 0.03161,-0.084282 0.03687,-0.036873 0.08955,-0.036873 h 0.331859 q 0.05268,0 0.08428,0.036873 0.03687,0.031606 0.03687,0.084282 v 0.2844504 q 0,0.052676 -0.03687,0.089549 -0.03161,0.031606 -0.08428,0.031606 z" />
+      <path
+         id="path3285"
+         d="m 34.226541,7.0891893 q -0.294985,0 -0.505689,-0.073746 -0.210704,-0.073746 -0.342394,-0.1790984 -0.13169,-0.105352 -0.200169,-0.210704 -0.06321,-0.105352 -0.06848,-0.1685632 -0.0053,-0.057944 0.03687,-0.089549 0.04214,-0.031606 0.08428,-0.031606 h 0.231774 q 0.03161,0 0.04741,0.010535 0.02107,0.00527 0.05268,0.042141 0.06848,0.073746 0.15276,0.1474928 0.08428,0.073746 0.205436,0.1211548 0.126423,0.047408 0.310789,0.047408 0.268647,0 0.442478,-0.1000844 0.173831,-0.105352 0.173831,-0.3055208 0,-0.13169 -0.07375,-0.210704 -0.06848,-0.079014 -0.252845,-0.1422252 -0.179099,-0.063211 -0.495155,-0.13169 -0.316056,-0.073746 -0.500422,-0.1790984 -0.184366,-0.1106196 -0.26338,-0.2581124 -0.07901,-0.1527604 -0.07901,-0.342394 0,-0.1949012 0.115888,-0.3739996 0.115887,-0.184366 0.337126,-0.3002532 0.226507,-0.1158872 0.563633,-0.1158872 0.273915,0 0.468817,0.068479 0.194901,0.068479 0.321323,0.1738308 0.126423,0.1000844 0.189634,0.2001688 0.06321,0.1000844 0.06848,0.1685632 0.0053,0.052676 -0.03161,0.089549 -0.03687,0.031606 -0.08428,0.031606 h -0.221239 q -0.03687,0 -0.06321,-0.015803 -0.02107,-0.015803 -0.04214,-0.036873 -0.05268,-0.068479 -0.126422,-0.1369576 -0.06848,-0.068479 -0.184366,-0.1106196 -0.11062,-0.047408 -0.294986,-0.047408 -0.26338,0 -0.39507,0.1106196 -0.13169,0.1106196 -0.13169,0.2791828 0,0.1000844 0.05794,0.1790984 0.05794,0.079014 0.221239,0.1422252 0.163296,0.063211 0.484619,0.1369576 0.347662,0.068479 0.547831,0.184366 0.200168,0.1158872 0.28445,0.2686476 0.08428,0.1527604 0.08428,0.3529292 0,0.2212392 -0.13169,0.4056052 -0.126423,0.184366 -0.379268,0.2949856 -0.247577,0.105352 -0.616309,0.105352 z" />
+      <path
+         id="path3287"
+         d="m 37.439282,7.0365133 q -0.05794,0 -0.08955,-0.031606 -0.03161,-0.036873 -0.03161,-0.089549 V 3.4756157 q 0,-0.057944 0.03161,-0.089549 0.03161,-0.036873 0.08955,-0.036873 h 1.443323 q 0.39507,0 0.642647,0.13169 0.252845,0.1264224 0.368732,0.3476616 0.121155,0.2212392 0.121155,0.4951544 0,0.2159716 -0.07375,0.3739996 -0.06848,0.1527604 -0.173831,0.2528448 -0.105352,0.094817 -0.205436,0.1474928 0.205436,0.1000844 0.363464,0.3265912 0.163296,0.2265068 0.163296,0.5478304 0,0.289718 -0.13169,0.5320276 -0.13169,0.2423096 -0.39507,0.3898024 -0.258112,0.1422252 -0.642647,0.1422252 z m 0.39507,-0.4266756 h 1.016647 q 0.342394,0 0.52676,-0.184366 0.184366,-0.184366 0.184366,-0.4530136 0,-0.2791828 -0.184366,-0.4582812 -0.184366,-0.184366 -0.52676,-0.184366 h -1.016647 z m 0,-1.7014348 h 0.969239 q 0.337126,0 0.510957,-0.1527604 0.173831,-0.158028 0.173831,-0.4266756 0,-0.2686476 -0.173831,-0.4108728 -0.173831,-0.1422252 -0.510957,-0.1422252 h -0.969239 z" />
+      <path
+         id="path3289"
+         d="m 41.532043,7.0891893 q -0.26338,0 -0.479351,-0.105352 Q 40.83672,6.8784853 40.70503,6.6993869 40.57334,6.5202885 40.57334,6.2937817 q 0,-0.3634644 0.294986,-0.579436 0.294985,-0.2159716 0.769069,-0.2844504 L 42.422268,5.3192757 V 5.1665153 q 0,-0.2528448 -0.147493,-0.39507 -0.142225,-0.1422252 -0.468816,-0.1422252 -0.237042,0 -0.384535,0.094817 -0.142225,0.094817 -0.200169,0.2423096 -0.03161,0.079014 -0.11062,0.079014 h -0.237042 q -0.05794,0 -0.08955,-0.031606 -0.02634,-0.036873 -0.02634,-0.084282 0,-0.079014 0.05794,-0.1949012 0.06321,-0.1158872 0.189633,-0.2265068 0.126423,-0.1106196 0.321324,-0.184366 0.200169,-0.079014 0.484619,-0.079014 0.316056,0 0.532028,0.084282 0.215971,0.079014 0.337126,0.2159716 0.126423,0.1369576 0.179099,0.3107884 0.05794,0.1738308 0.05794,0.3529292 v 1.7067024 q 0,0.052676 -0.03687,0.089549 -0.03161,0.031606 -0.08428,0.031606 h -0.242309 q -0.05794,0 -0.08955,-0.031606 -0.03161,-0.036873 -0.03161,-0.089549 V 6.6888517 q -0.06848,0.094817 -0.184366,0.1896336 -0.115887,0.089549 -0.289718,0.1527604 -0.173831,0.057944 -0.426676,0.057944 z m 0.11062,-0.39507 q 0.215972,0 0.39507,-0.089549 0.179098,-0.094817 0.279183,-0.289718 0.105352,-0.1949012 0.105352,-0.4898868 V 5.6774725 l -0.611042,0.089549 q -0.373999,0.052676 -0.563633,0.1790984 -0.189634,0.1211548 -0.189634,0.3107884 0,0.1474928 0.08428,0.2475772 0.08955,0.094817 0.221239,0.1422252 0.136958,0.047408 0.279183,0.047408 z" />
+      <path
+         id="path3291"
+         d="m 43.800979,7.0365133 q -0.05268,0 -0.08955,-0.031606 -0.03161,-0.036873 -0.03161,-0.089549 V 4.4237837 q 0,-0.052676 0.03161,-0.089549 0.03687,-0.036873 0.08955,-0.036873 h 0.24231 q 0.05268,0 0.08955,0.036873 0.03687,0.036873 0.03687,0.089549 v 0.2317744 q 0.105352,-0.1790984 0.289718,-0.2686476 0.184366,-0.089549 0.442479,-0.089549 h 0.210704 q 0.05268,0 0.08428,0.036873 0.03161,0.031606 0.03161,0.084282 v 0.2159716 q 0,0.052676 -0.03161,0.084282 -0.03161,0.031606 -0.08428,0.031606 h -0.316056 q -0.284451,0 -0.447746,0.1685632 -0.163296,0.1632956 -0.163296,0.447746 v 1.5486744 q 0,0.052676 -0.03687,0.089549 -0.03687,0.031606 -0.08955,0.031606 z" />
+      <path
+         id="path3293"
+         d="m 46.724992,7.0891893 q -0.542562,0 -0.863886,-0.3318588 -0.321324,-0.3371264 -0.352929,-0.9165624 -0.0053,-0.068479 -0.0053,-0.1738308 0,-0.1106196 0.0053,-0.1790984 0.02107,-0.3739996 0.173831,-0.6531824 0.15276,-0.2844504 0.41614,-0.4372108 0.268648,-0.1527604 0.626844,-0.1527604 0.400338,0 0.668986,0.1685632 0.273915,0.1685632 0.41614,0.4793516 0.142225,0.3107884 0.142225,0.7269288 v 0.089549 q 0,0.057944 -0.03687,0.089549 -0.03161,0.031606 -0.08428,0.031606 h -1.822589 q 0,0.00527 0,0.02107 0,0.015803 0,0.026338 0.01054,0.2159716 0.09482,0.4056052 0.08428,0.184366 0.242309,0.3002532 0.158028,0.1158872 0.379267,0.1158872 0.189634,0 0.316056,-0.057944 0.126423,-0.057944 0.205437,-0.1264224 0.07901,-0.073746 0.105352,-0.1106196 0.04741,-0.068479 0.07375,-0.079014 0.02634,-0.015803 0.08428,-0.015803 h 0.252845 q 0.05268,0 0.08428,0.031606 0.03687,0.026338 0.03161,0.079014 -0.0053,0.079014 -0.08428,0.1949012 -0.07901,0.1106196 -0.226507,0.2212392 -0.147492,0.1106196 -0.358196,0.184366 -0.210704,0.068479 -0.48462,0.068479 z M 46.008599,5.4667685 h 1.443322 v -0.015803 q 0,-0.237042 -0.08955,-0.421408 -0.08428,-0.184366 -0.247577,-0.289718 -0.163296,-0.1106196 -0.389803,-0.1106196 -0.226506,0 -0.389802,0.1106196 -0.158028,0.105352 -0.24231,0.289718 -0.08428,0.184366 -0.08428,0.421408 z" />
+      <path
+         id="path3295"
+         d="m 17.984143,13.621014 q -0.05268,0 -0.08955,-0.03161 -0.03161,-0.03687 -0.03161,-0.08955 v -3.439743 q 0,-0.05794 0.03161,-0.089549 0.03687,-0.036873 0.08955,-0.036873 h 0.26338 q 0.06321,0 0.09482,0.031606 0.0316,0.031606 0.03687,0.052676 l 1.116731,2.128111 1.121999,-2.128111 q 0.01054,-0.02107 0.03687,-0.052676 0.03161,-0.031606 0.09482,-0.031606 h 0.258112 q 0.05794,0 0.08955,0.036873 0.03687,0.031605 0.03687,0.089549 v 3.439743 q 0,0.05268 -0.03687,0.08955 -0.03161,0.03161 -0.08955,0.03161 h -0.258112 q -0.05268,0 -0.08955,-0.03161 -0.03161,-0.03687 -0.03161,-0.08955 V 10.9082 l -0.879689,1.717237 q -0.02107,0.05268 -0.06321,0.08428 -0.04214,0.02634 -0.105352,0.02634 H 19.41693 q -0.06848,0 -0.110619,-0.02634 -0.03687,-0.03161 -0.06321,-0.08428 L 18.36341,10.9082 v 2.591659 q 0,0.05268 -0.03687,0.08955 -0.03161,0.03161 -0.08428,0.03161 z" />
+      <path
+         id="path3297"
+         d="m 23.072233,13.67369 q -0.542563,0 -0.863886,-0.331859 -0.321324,-0.337127 -0.352929,-0.916563 -0.0053,-0.06848 -0.0053,-0.17383 0,-0.11062 0.0053,-0.179099 0.02107,-0.373999 0.17383,-0.653182 0.152761,-0.284451 0.416141,-0.437211 0.268647,-0.15276 0.626844,-0.15276 0.400338,0 0.668986,0.168563 0.273915,0.168563 0.41614,0.479351 0.142225,0.310789 0.142225,0.726929 v 0.08955 q 0,0.05794 -0.03687,0.08955 -0.03161,0.03161 -0.08428,0.03161 H 22.35584 q 0,0.0053 0,0.02107 0,0.0158 0,0.02634 0.01054,0.215971 0.09482,0.405605 0.08428,0.184366 0.24231,0.300253 0.158028,0.115887 0.379267,0.115887 0.189634,0 0.316056,-0.05794 0.126423,-0.05794 0.205437,-0.126423 0.07901,-0.07375 0.105352,-0.110619 0.04741,-0.06848 0.07375,-0.07901 0.02634,-0.0158 0.08428,-0.0158 h 0.252845 q 0.05268,0 0.08428,0.0316 0.03687,0.02634 0.03161,0.07901 -0.0053,0.07901 -0.08428,0.194902 -0.07901,0.110619 -0.226507,0.221239 -0.147492,0.110619 -0.358197,0.184366 -0.210704,0.06848 -0.484619,0.06848 z M 22.35584,12.051269 h 1.443322 v -0.0158 q 0,-0.237042 -0.08955,-0.421408 -0.08428,-0.184366 -0.247577,-0.289718 -0.163296,-0.11062 -0.389803,-0.11062 -0.226507,0 -0.389802,0.11062 -0.158028,0.105352 -0.24231,0.289718 -0.08428,0.184366 -0.08428,0.421408 z" />
+      <path
+         id="path3299"
+         d="m 25.983323,13.621014 q -0.294986,0 -0.479352,-0.11062 -0.184366,-0.115887 -0.268647,-0.321324 -0.08428,-0.210704 -0.08428,-0.495154 V 11.30327 h -0.410873 q -0.05268,0 -0.08955,-0.03161 -0.03161,-0.03687 -0.03161,-0.08955 v -0.179099 q 0,-0.05268 0.03161,-0.08428 0.03687,-0.03687 0.08955,-0.03687 h 0.410873 v -0.87969 q 0,-0.052676 0.03161,-0.084281 0.03687,-0.036873 0.08955,-0.036873 h 0.247577 q 0.05268,0 0.08428,0.036873 0.03687,0.031606 0.03687,0.084281 v 0.87969 h 0.653182 q 0.05268,0 0.08428,0.03687 0.03687,0.03161 0.03687,0.08428 v 0.179099 q 0,0.05268 -0.03687,0.08955 -0.03161,0.03161 -0.08428,0.03161 h -0.653182 v 1.353773 q 0,0.247577 0.08428,0.389802 0.08428,0.142225 0.300254,0.142225 h 0.321323 q 0.05268,0 0.08428,0.03687 0.03687,0.0316 0.03687,0.08428 v 0.189634 q 0,0.05268 -0.03687,0.08955 -0.03161,0.03161 -0.08428,0.03161 z" />
+      <path
+         id="path3301"
+         d="m 27.735458,13.67369 q -0.26338,0 -0.479351,-0.105352 -0.215972,-0.105352 -0.347662,-0.284451 -0.13169,-0.179098 -0.13169,-0.405605 0,-0.363464 0.294986,-0.579436 0.294985,-0.215972 0.769069,-0.28445 l 0.784873,-0.11062 v -0.15276 q 0,-0.252845 -0.147493,-0.39507 -0.142225,-0.142226 -0.468817,-0.142226 -0.237042,0 -0.384534,0.09482 -0.142226,0.09482 -0.200169,0.24231 -0.03161,0.07901 -0.11062,0.07901 h -0.237042 q -0.05794,0 -0.08955,-0.03161 -0.02634,-0.03687 -0.02634,-0.08428 0,-0.07901 0.05794,-0.194902 0.06321,-0.115887 0.189633,-0.226506 0.126423,-0.11062 0.321324,-0.184366 0.200169,-0.07901 0.484619,-0.07901 0.316056,0 0.532028,0.08428 0.215971,0.07901 0.337126,0.215972 0.126422,0.136957 0.179098,0.310788 0.05794,0.173831 0.05794,0.352929 v 1.706703 q 0,0.05268 -0.03687,0.08955 -0.03161,0.03161 -0.08428,0.03161 h -0.242309 q -0.05794,0 -0.08955,-0.03161 -0.0316,-0.03687 -0.0316,-0.08955 v -0.226507 q -0.06848,0.09482 -0.184366,0.189634 -0.115887,0.08955 -0.289718,0.15276 -0.173831,0.05794 -0.426676,0.05794 z m 0.11062,-0.39507 q 0.215971,0 0.39507,-0.08955 0.179098,-0.09482 0.279183,-0.289718 0.105352,-0.194901 0.105352,-0.489886 v -0.147493 l -0.611042,0.08955 q -0.374,0.05268 -0.563633,0.179098 -0.189634,0.121155 -0.189634,0.310789 0,0.147493 0.08428,0.247577 0.08955,0.09482 0.221239,0.142225 0.136958,0.04741 0.279183,0.04741 z" />
+      <path
+         id="path3303"
+         d="m 30.009663,13.621014 q -0.05268,0 -0.08955,-0.03161 -0.0316,-0.03687 -0.0316,-0.08955 v -3.497687 q 0,-0.052676 0.0316,-0.084281 0.03687,-0.036873 0.08955,-0.036873 h 0.247577 q 0.05794,0 0.08955,0.036873 0.03161,0.031606 0.03161,0.084281 v 3.497687 q 0,0.05268 -0.03161,0.08955 -0.03161,0.03161 -0.08955,0.03161 z" />
+      <path
+         id="path3305"
+         d="m 32.634656,13.621014 q -0.05794,0 -0.08955,-0.03161 -0.0316,-0.03687 -0.0316,-0.08955 v -3.439743 q 0,-0.05794 0.0316,-0.089549 0.03161,-0.036873 0.08955,-0.036873 h 0.268647 q 0.05794,0 0.08955,0.036873 0.03687,0.031605 0.03687,0.089549 v 1.453858 h 1.812054 v -1.453858 q 0,-0.05794 0.03161,-0.089549 0.03687,-0.036873 0.08955,-0.036873 h 0.268647 q 0.05794,0 0.08955,0.036873 0.03687,0.031605 0.03687,0.089549 v 3.439743 q 0,0.05268 -0.03687,0.08955 -0.03161,0.03161 -0.08955,0.03161 h -0.268647 q -0.05268,0 -0.08955,-0.03161 -0.03161,-0.03687 -0.03161,-0.08955 v -1.511801 h -1.812054 v 1.511801 q 0,0.05268 -0.03687,0.08955 -0.0316,0.03161 -0.08955,0.03161 z" />
+      <path
+         id="path3307"
+         d="m 37.222159,13.67369 q -0.337127,0 -0.574169,-0.147493 -0.231774,-0.152761 -0.352929,-0.421408 -0.121155,-0.268648 -0.121155,-0.616309 v -1.485464 q 0,-0.05268 0.03161,-0.08428 0.03687,-0.03687 0.08955,-0.03687 h 0.26338 q 0.05268,0 0.08428,0.03687 0.03687,0.03161 0.03687,0.08428 v 1.459126 q 0,0.784872 0.684788,0.784872 0.326591,0 0.521492,-0.205436 0.200169,-0.210704 0.200169,-0.579436 v -1.459126 q 0,-0.05268 0.03161,-0.08428 0.03687,-0.03687 0.08955,-0.03687 h 0.258112 q 0.05794,0 0.08955,0.03687 0.0316,0.03161 0.0316,0.08428 v 2.496843 q 0,0.05268 -0.0316,0.08955 -0.03161,0.03161 -0.08955,0.03161 h -0.242309 q -0.05268,0 -0.08955,-0.03161 -0.03161,-0.03687 -0.03161,-0.08955 v -0.231775 q -0.142225,0.184366 -0.347662,0.294986 -0.200168,0.11062 -0.532027,0.11062 z" />
+      <path
+         id="path3309"
+         d="m 40.722808,13.67369 q -0.316056,0 -0.52676,-0.11062 -0.210704,-0.115887 -0.337126,-0.28445 v 0.221239 q 0,0.05268 -0.03687,0.08955 -0.03161,0.03161 -0.08428,0.03161 h -0.242309 q -0.05268,0 -0.08955,-0.03161 -0.0316,-0.03687 -0.0316,-0.08955 v -3.497687 q 0,-0.052676 0.0316,-0.084281 0.03687,-0.036873 0.08955,-0.036873 h 0.252844 q 0.05794,0 0.08955,0.036873 0.0316,0.031606 0.0316,0.084281 v 1.211548 q 0.13169,-0.163295 0.337127,-0.273915 0.210704,-0.110619 0.516224,-0.110619 0.294986,0 0.500422,0.105352 0.210704,0.100084 0.342394,0.279182 0.136958,0.179099 0.205437,0.405606 0.06848,0.226506 0.07375,0.474084 0.0053,0.08428 0.0053,0.158028 0,0.07375 -0.0053,0.158028 -0.0053,0.252844 -0.07375,0.479351 -0.06848,0.226507 -0.205437,0.405605 -0.13169,0.173831 -0.342394,0.279183 -0.205436,0.100085 -0.500422,0.100085 z m -0.110619,-0.426676 q 0.273915,0 0.426675,-0.115887 0.158028,-0.121155 0.226507,-0.310789 0.06848,-0.194901 0.07901,-0.41614 0.0053,-0.15276 0,-0.305521 -0.01054,-0.221239 -0.07901,-0.410873 -0.06848,-0.194901 -0.226507,-0.310788 -0.15276,-0.121155 -0.426675,-0.121155 -0.247577,0 -0.410873,0.115887 -0.163296,0.115888 -0.247577,0.294986 -0.07901,0.179098 -0.08428,0.363464 -0.0053,0.08428 -0.0053,0.205437 0,0.115887 0.0053,0.200169 0.01054,0.194901 0.08428,0.384534 0.07901,0.184366 0.237042,0.305521 0.163295,0.121155 0.421408,0.121155 z" />
+    </g>
+    <g
        style="font-size:10.5833px;line-height:1.25;font-family:Rubik;-inkscape-font-specification:'Rubik, Normal';stroke-width:0.264583"
-       x="29.132378"
-       y="29.666916"><tspan
-         x="29.132378"
-         y="29.666916"
-         style="stroke-width:0.264583" /></text>
+       id="text2656" />
   </g>
   <g
+     id="g2700"
      transform="matrix(0.02896941,0,0,0.02896941,0.43488052,1.0504833)">
     <path
-       fill="#465a61"
-       d="m 48.975,117.956 h 414.05 V 429.215 H 48.975 Z" />
+       id="path2660"
+       d="m 48.975,117.956 h 414.05 V 429.215 H 48.975 Z"
+       fill="#465a61" />
     <path
-       fill="#7a8c98"
-       d="M 482.834,512 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 375.225 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 V 499.478 C 495.355,506.394 489.749,512 482.834,512 Z" />
+       id="path2662"
+       d="M 482.834,512 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 375.225 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 V 499.478 C 495.355,506.394 489.749,512 482.834,512 Z"
+       fill="#7a8c98" />
     <path
-       fill="#9facba"
-       d="M 282.391,459.506 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z" />
+       id="path2664"
+       d="M 282.391,459.506 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
+       fill="#9facba" />
     <path
-       fill="#ffd15b"
-       d="m 376.886,474.917 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.569 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z" />
+       id="path2666"
+       d="m 376.886,474.917 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.569 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
+       fill="#ffd15b" />
     <path
-       fill="#7a8c98"
-       d="M 482.834,331.356 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 194.582 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z" />
+       id="path2668"
+       d="M 482.834,331.356 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 194.582 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
+       fill="#7a8c98" />
     <path
-       fill="#9facba"
-       d="M 282.391,278.862 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z" />
+       id="path2670"
+       d="M 282.391,278.862 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
+       fill="#9facba" />
     <path
-       fill="#60b7ff"
-       d="m 376.886,294.273 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.569 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z" />
+       id="path2672"
+       d="m 376.886,294.273 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.569 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
+       fill="#60b7ff" />
     <path
-       fill="#7a8c98"
-       d="M 482.834,149.296 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 12.522 C 16.645,5.606 22.251,0 29.166,0 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z" />
+       id="path2674"
+       d="M 482.834,149.296 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 12.522 C 16.645,5.606 22.251,0 29.166,0 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
+       fill="#7a8c98" />
     <path
-       fill="#9facba"
-       d="M 282.391,96.802 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z" />
+       id="path2676"
+       d="M 282.391,96.802 H 71.129 c -12.235,0 -22.154,-9.919 -22.154,-22.154 0,-12.235 9.919,-22.154 22.154,-22.154 h 211.262 c 12.235,0 22.154,9.919 22.154,22.154 -0.001,12.235 -9.919,22.154 -22.154,22.154 z"
+       fill="#9facba" />
     <path
-       fill="#60b7ff"
-       d="m 376.886,112.213 c -9.544,0 -17.28,-7.737 -17.28,-17.28 V 54.364 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z" />
+       id="path2678"
+       d="m 376.886,112.213 c -9.544,0 -17.28,-7.737 -17.28,-17.28 V 54.364 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.569 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
+       fill="#60b7ff" />
     <g
+       id="g2686"
        fill="#596c76">
       <path
+         id="path2680"
          d="m 482.834,362.704 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 375.225 c -10e-4,-6.915 -5.607,-12.521 -12.522,-12.521 z" />
       <path
+         id="path2682"
          d="m 482.834,182.06 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.915 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 194.582 c -10e-4,-6.916 -5.607,-12.522 -12.522,-12.522 z" />
       <path
+         id="path2684"
          d="m 482.834,0 h -39.685 c 6.916,0 12.522,5.606 12.522,12.522 v 124.253 c 0,6.916 -5.606,12.522 -12.522,12.522 h 39.685 c 6.916,0 12.522,-5.606 12.522,-12.522 V 12.522 C 495.355,5.606 489.749,0 482.834,0 Z" />
     </g>
     <path
-       fill="#ffd15b"
-       d="m 445.745,37.083 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.022 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.57 c 0,-9.544 -7.737,-17.281 -17.28,-17.281 z" />
+       id="path2688"
+       d="m 445.745,37.083 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.022 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.57 c 0,-9.544 -7.737,-17.281 -17.28,-17.281 z"
+       fill="#ffd15b" />
     <path
-       fill="#ffc344"
-       d="m 455.67,40.232 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 V 54.364 c 10e-4,-5.847 -2.91,-11.005 -7.354,-14.132 z" />
+       id="path2690"
+       d="m 455.67,40.232 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 V 54.364 c 10e-4,-5.847 -2.91,-11.005 -7.354,-14.132 z"
+       fill="#ffc344" />
     <path
-       fill="#fe646f"
-       d="m 445.745,219.143 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.569 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z" />
+       id="path2692"
+       d="m 445.745,219.143 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.569 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
+       fill="#fe646f" />
     <path
-       fill="#fd4755"
-       d="m 455.67,222.292 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 v -40.569 c 10e-4,-5.846 -2.91,-11.004 -7.354,-14.131 z" />
+       id="path2694"
+       d="m 455.67,222.292 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 v -40.569 c 10e-4,-5.846 -2.91,-11.004 -7.354,-14.131 z"
+       fill="#fd4755" />
     <path
-       fill="#60b7ff"
-       d="m 445.745,399.787 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.569 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z" />
+       id="path2696"
+       d="m 445.745,399.787 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.569 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.569 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
+       fill="#60b7ff" />
     <path
-       fill="#26a6fe"
-       d="m 455.67,402.936 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 v -40.569 c 10e-4,-5.846 -2.91,-11.004 -7.354,-14.131 z" />
+       id="path2698"
+       d="m 455.67,402.936 v 68.832 c 4.443,-3.127 7.354,-8.285 7.354,-14.132 v -40.569 c 10e-4,-5.846 -2.91,-11.004 -7.354,-14.131 z"
+       fill="#26a6fe" />
   </g>
 </svg>

+ 4 - 4
server/main.ts

@@ -15,8 +15,8 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import express from 'express'
 import path from 'path'
 
-import router from './api/router'
-import azureProxy from './azureProxy'
+import apiRouter from './api/router'
+import proxyRouter from './proxies/router'
 
 export default () => {
   const app = express()
@@ -25,9 +25,9 @@ export default () => {
 
   app.use(express.static('dist'))
 
-  azureProxy(app)
+  app.use('/proxy', proxyRouter)
 
-  app.use('/api', router)
+  app.use('/api', apiRouter)
 
   app.get('*', (_, res) => {
     res.sendFile(path.resolve(__dirname, '../dist', 'index.html'))

+ 3 - 6
server/azureProxy.ts → server/proxies/azureProxy.ts

@@ -15,7 +15,6 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import express from 'express'
 
 import MsRest from 'ms-rest-azure'
-import bodyParser from 'body-parser'
 import axios from 'axios'
 
 const forwardHeaders = ['authorization']
@@ -24,10 +23,8 @@ const buildError = (message: any) => ({
   error: { message: `Proxy - ${message}` },
 })
 
-export default (app: express.Application) => {
-  const jsonParser = bodyParser.json()
-
-  app.post('/azure-login', jsonParser, (req, res) => {
+export default (router: express.Router) => {
+  router.post('/azure/login', (req, res) => {
     const handleResponse = (err: any, credentials: any) => {
       if (err) {
         console.log(err)
@@ -48,7 +45,7 @@ export default (app: express.Application) => {
     }
   })
 
-  app.get('/proxy/*', (req, res) => {
+  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 = {}

+ 35 - 0
server/proxies/metalHubProxy.ts

@@ -0,0 +1,35 @@
+/*
+Copyright (C) 2022  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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'
+
+const buildError = (message: any) => ({
+  error: { message },
+})
+
+export default (router: express.Router) => {
+  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
+    }
+    try {
+      res.json(fs.readFileSync(path, 'utf8'))
+    } catch (err) {
+      res.status(500).json(buildError(err.message))
+    }
+  })
+}

+ 27 - 0
server/proxies/router.ts

@@ -0,0 +1,27 @@
+/*
+Copyright (C) 2022  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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'
+
+const router = express.Router()
+
+router.use(bodyParser.json())
+
+azureProxy(router)
+metalHubProxy(router)
+
+export default router

+ 3 - 0
src/@types/Config.ts

@@ -20,6 +20,7 @@ export type Services = {
   coriolisLogs: string,
   coriolisLogStreamBaseUrl: string,
   coriolisLicensing: string,
+  metalhub: string,
   cloudbaseEmailEndpoint: string
 }
 
@@ -35,9 +36,11 @@ export type Config = {
   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,
 }

+ 41 - 0
src/@types/MetalHub.ts

@@ -0,0 +1,41 @@
+export type MetalHubDisk = {
+  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,
+  }[],
+}
+
+export type MetalHubNic = {
+  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,
+  os_info: {
+    os_name: string,
+    os_version: string,
+  },
+  disks?: MetalHubDisk[],
+  nics?: MetalHubNic[],
+  physical_cores?: number,
+  logical_cores?: number,
+}

+ 4 - 0
src/components/App.tsx

@@ -28,6 +28,7 @@ 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'
@@ -50,6 +51,7 @@ 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}
@@ -210,6 +212,8 @@ class App extends React.Component<{}, State> {
             {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' })}

+ 1 - 1
src/components/modules/DetailsModule/DetailsContentHeader/DetailsContentHeader.tsx

@@ -19,7 +19,7 @@ import { Link } from 'react-router-dom'
 
 import StatusPill from '@src/components/ui/StatusComponents/StatusPill'
 import ActionDropdown from '@src/components/ui/Dropdowns/ActionDropdown'
-import type { Action as DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
+import type { DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
 
 import { ThemePalette, ThemeProps } from '@src/components/Theme'
 

+ 77 - 0
src/components/modules/MetalHubModule/MetalHubListHeader/MetalHubListHeader.tsx

@@ -0,0 +1,77 @@
+/*
+Copyright (C) 2022  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 CopyValue from '@src/components/ui/CopyValue'
+import Button from '@src/components/ui/Button'
+import { ThemePalette } from '@src/components/Theme'
+import InfoIcon from '@src/components/ui/InfoIcon'
+
+const Wrapper = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  margin-top: -70px;
+  margin-bottom: 32px;
+`
+const FingerPrint = styled.div`
+  margin-right: 32px;
+`
+const FingerPrintLabel = styled.div`
+  font-size: 10px;
+  font-weight: 500;
+  color: ${ThemePalette.grayscale[3]};
+  text-transform: uppercase;
+  margin-bottom: 3px;
+`
+const FingerPrintValue = styled.div``
+
+type Props = {
+  hideButton: boolean
+  error: string
+  fingerprint: string
+  onCreateClick: () => void
+}
+
+@observer
+class MetalHubListHeader extends React.Component<Props> {
+  renderContent() {
+    let valueContent: React.ReactNode = this.props.error
+
+    if (!this.props.error) {
+      const cleanFingerprint = this.props.fingerprint.replace(/\n/g, '')
+      const fingerprintShortened = `${cleanFingerprint.slice(0, 12)}...${cleanFingerprint.slice(-12)}`
+      valueContent = <CopyValue value={cleanFingerprint} label={fingerprintShortened} />
+    }
+    return (
+      <FingerPrint>
+        <FingerPrintLabel>Coriolis Bare Metal Hub Fingerprint <InfoIcon text="The fingerprint is used when installing the agent on the Bare Metal" /> </FingerPrintLabel>
+        <FingerPrintValue>{valueContent}</FingerPrintValue>
+      </FingerPrint>
+    )
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        {this.renderContent()}
+        {!this.props.hideButton ? <Button hollow onClick={this.props.onCreateClick}>Add a Bare Metal Server</Button> : null}
+      </Wrapper>
+    )
+  }
+}
+
+export default MetalHubListHeader

+ 6 - 0
src/components/modules/MetalHubModule/MetalHubListHeader/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "MetalHubListHeader",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./MetalHubListHeader.tsx"
+}

+ 133 - 0
src/components/modules/MetalHubModule/MetalHubListItem/MetalHubListItem.tsx

@@ -0,0 +1,133 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 { ThemePalette, ThemeProps } from '@src/components/Theme'
+
+import { MetalHubServer } from '@src/@types/MetalHub'
+import moment from 'moment'
+import StatusPill from '@src/components/ui/StatusComponents/StatusPill'
+import serverImage from './images/server.svg'
+
+const Content = styled.div<any>`
+  display: flex;
+  align-items: center;
+  border-top: 1px solid ${ThemePalette.grayscale[1]};
+  padding: 8px 16px;
+  cursor: pointer;
+  flex-grow: 1;
+  transition: all ${ThemeProps.animations.swift};
+  min-width: 785px;
+
+  &:hover {
+    background: ${ThemePalette.grayscale[1]};
+  }
+`
+const Wrapper = styled.div<any>`
+  display: flex;
+  align-items: center;
+
+  &:last-child ${Content} {
+    border-bottom: 1px solid ${ThemePalette.grayscale[1]};
+  }
+`
+const Image = styled.div`
+  min-width: 48px;
+  height: 48px;
+  background: url('${serverImage}') no-repeat center;
+  margin-right: 16px;
+`
+const Title = styled.div`
+  flex-grow: 1;
+  overflow: hidden;
+  margin-right: 48px;
+  min-width: 100px;
+`
+const TitleLabel = styled.div`
+  font-size: 16px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`
+const ItemLabel = styled.div`
+  color: ${ThemePalette.grayscale[4]};
+`
+const ItemValue = styled.div`
+  color: ${ThemePalette.primary};
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+`
+const Body = styled.div`
+  ${ThemeProps.exactWidth('642px')}
+  display: flex;
+`
+const Data = styled.div<{ width: number }>`
+  width: ${props => props.width}px;
+  margin: 0 32px;
+
+  &:last-child {
+    margin-right: 0;
+  }
+`
+
+type Props = {
+  item: MetalHubServer,
+  onClick: () => void,
+}
+@observer
+class MetalHubServerListItem extends React.Component<Props> {
+  render() {
+    return (
+      <Wrapper>
+        <Content onClick={this.props.onClick}>
+          <Image />
+          <Title>
+            <TitleLabel>{this.props.item.hostname || 'No Hostname'}</TitleLabel>
+            {this.props.item.active ? (
+              <StatusPill style={{ marginTop: '4px' }} status="COMPLETED" label="Active" />
+            ) : (
+              <StatusPill style={{ marginTop: '4px' }} status="ERROR" label="Inactive" />
+            )}
+          </Title>
+          <Body>
+            <Data width={500}>
+              <ItemLabel>API Endpoint</ItemLabel>
+              <ItemValue>
+                {this.props.item.api_endpoint}
+              </ItemValue>
+            </Data>
+            <Data width={145}>
+              <ItemLabel>Created At</ItemLabel>
+              <ItemValue>
+                {moment(this.props.item.created_at).format('YYYY-MM-DD HH:mm:ss')}
+              </ItemValue>
+            </Data>
+            <Data width={145}>
+              <ItemLabel>Updated At</ItemLabel>
+              <ItemValue>
+                {moment(this.props.item.updated_at).format('YYYY-MM-DD HH:mm:ss')}
+              </ItemValue>
+            </Data>
+          </Body>
+        </Content>
+      </Wrapper>
+    )
+  }
+}
+
+export default MetalHubServerListItem

+ 92 - 0
src/components/modules/MetalHubModule/MetalHubListItem/images/server.svg

@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg    xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   id="svg8"
+   version="1.1"
+   viewBox="0 0 12.7 12.7"
+   height="48"
+   width="48">
+  <defs
+     id="defs2">
+    <path
+       d="M24,48 C37.254834,48 48,37.254834 48,24 C48,10.745166 37.254834,0 24,0 C10.745166,0 0,10.745166 0,24 C0,37.254834 10.745166,48 24,48 Z"
+       id="path-1" />
+  </defs>
+  <g
+     id="layer1"
+     >
+    <g
+       transform="scale(0.26458333)"
+       style="fill:none;fill-rule:evenodd;stroke:none;stroke-width:1"
+       id="Icon/Project/ProjectListItem">
+      <mask
+         fill="#ffffff"
+         id="mask-2">
+        <use
+           height="100%"
+           width="100%"
+           y="0"
+           x="0"
+           id="use838"
+           xlink:href="#path-1" />
+      </mask>
+      <use
+         height="100%"
+         width="100%"
+         y="0"
+         x="0"
+         xlink:href="#path-1"
+         fill-rule="evenodd"
+         fill="#c8ccd7"
+         id="Pat-Benetar" />
+      <g
+         transform="matrix(0.06333812,0,0,0.06333812,7.7854416,7.7334707)"
+         id="g1545">
+        <path
+           fill="#7a8c98"
+           d="M 482.834,445.69221 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 v -124.253 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.916 -5.607,12.522 -12.522,12.522 z"
+           style="fill:#0044ca;fill-opacity:1"
+           id="path1507" />
+        <path
+           fill="#7a8c98"
+           d="M 482.834,217.24483 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 80.470853 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 V 204.72383 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
+           style="fill:#0044ca;fill-opacity:1"
+           id="path1513" />
+        <path
+           fill="#9facba"
+           d="M 317.60028,182.09881 H 74.470786 c -14.08057,0 -25.495786,-16.82319 -25.495786,-37.5744 0,-20.75125 11.415216,-37.57443 25.495786,-37.57443 H 317.60028 c 14.08056,0 25.49578,16.82318 25.49578,37.57443 -0.001,20.75121 -11.41522,37.5744 -25.49578,37.5744 z"
+           style="fill:#c8ccd7;fill-opacity:1;stroke-width:1.3971"
+           id="path1515" />
+        <path
+           fill="#60b7ff"
+           d="m 384.5962,182.08938 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.56897 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.56897 c 0.001,9.543 -7.736,17.28 -17.28,17.28 z"
+           style="fill:#c8ccd7;fill-opacity:1"
+           id="path1517" />
+        <path
+           fill="#fe646f"
+           d="m 445.745,106.95941 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.56897 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.56897 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
+           style="fill:#c8ccd7;fill-opacity:1"
+           id="path1537" />
+        <path
+           id="path1515-2"
+           style="fill:#c8ccd7;fill-opacity:1;stroke-width:1.3971"
+           d="M 315.11203,408.88257 H 71.982511 c -14.080571,0 -25.495788,-16.82319 -25.495788,-37.57444 0,-20.75125 11.415217,-37.57443 25.495788,-37.57443 H 315.11203 c 14.08056,0 25.49579,16.82318 25.49579,37.57443 -9.9e-4,20.75125 -11.41523,37.57444 -25.49579,37.57444 z"
+           fill="#9facba" />
+        <path
+           id="path1517-7"
+           style="fill:#c8ccd7;fill-opacity:1;stroke-width:1"
+           d="m 390.15768,405.01754 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.56901 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.56901 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
+           fill="#60b7ff" />
+        <path
+           id="path1537-1"
+           style="fill:#c8ccd7;fill-opacity:1;stroke-width:1"
+           d="m 449.91183,329.11647 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.22699 v 40.56901 c 0,9.18501 7.172,16.67501 16.218,17.22701 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28001 v -40.56898 c 0,-9.54499 -7.737,-17.282 -17.28,-17.28199 z"
+           fill="#fe646f" />
+      </g>
+    </g>
+  </g>
+</svg>

+ 6 - 0
src/components/modules/MetalHubModule/MetalHubListItem/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "MetalHubListItem",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./MetalHubListItem.tsx"
+}

+ 224 - 0
src/components/modules/MetalHubModule/MetalHubModal/MetalHubModal.tsx

@@ -0,0 +1,224 @@
+/*
+Copyright (C) 2022  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 type { Field as FieldType } from '@src/@types/Field'
+import Button from '@src/components/ui/Button'
+import Modal from '@src/components/ui/Modal'
+import FieldInput from '@src/components/ui/FieldInput'
+
+import KeyboardManager from '@src/utils/KeyboardManager'
+import { ThemeProps } from '@src/components/Theme'
+import LoadingButton from '@src/components/ui/LoadingButton'
+import { MetalHubServer } from '@src/@types/MetalHub'
+import image from './images/server.svg'
+
+const Wrapper = styled.div`
+  padding: 48px 32px 32px 32px;
+`
+const Image = styled.div`
+  width: 96px;
+  height: 96px;
+  background: url('${image}') center no-repeat;
+  margin: 0 auto;
+`
+const Form = styled.div`
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  margin-top: 64px;
+
+  > div {
+    margin-top: 16px;
+  }
+`
+const Buttons = styled.div`
+  margin-top: 32px;
+  display: flex;
+  justify-content: space-between;
+`
+
+type Props = {
+  loading: boolean,
+  onRequestClose: () => void,
+} & (
+  {
+    server: MetalHubServer,
+    onEditClick: (apiEndpoint: string) => void,
+  } | {
+    server?: undefined,
+    onAddClick: (apiEndpoint: string) => void,
+  }
+)
+
+type State = {
+  host: string,
+  port: string,
+  highlightFieldNames: string[],
+}
+@observer
+class MetalHubModal extends React.Component<Props, State> {
+  state: State = {
+    host: '',
+    port: '',
+    highlightFieldNames: [],
+  }
+
+  componentDidMount() {
+    KeyboardManager.onEnter('MetalHubNewModal', () => {
+      this.handleAddClick()
+    }, 2)
+
+    if (this.props.server) {
+      const apiEndpointComponents = this.props.server.api_endpoint.split(':')
+      this.setState({
+        host: apiEndpointComponents[1].replace('//', ''),
+        port: apiEndpointComponents[2].replace(/\/.*/, ''),
+      })
+    }
+  }
+
+  componentWillUnmount() {
+    KeyboardManager.removeKeyDown('MetalHubNewModal')
+  }
+
+  handleAddClick() {
+    if (this.highlightFields()) {
+      return
+    }
+
+    const endpointUrl = `https://${this.state.host}:${this.state.port}/api/v1`
+    if (this.props.server) {
+      this.props.onEditClick(endpointUrl)
+    } else {
+      this.props.onAddClick(endpointUrl)
+    }
+  }
+
+  highlightFields() {
+    const highlightFieldNames = []
+    if (!this.state.host) {
+      highlightFieldNames.push('host')
+    }
+    if (!this.state.port) {
+      highlightFieldNames.push('port')
+    }
+    if (highlightFieldNames.length > 0) {
+      this.setState({ highlightFieldNames })
+      return true
+    }
+    this.setState({ highlightFieldNames: [] })
+    return false
+  }
+
+  renderField(opts: {
+    field: FieldType,
+    value: any,
+    onChange: (value: any) => void
+  }) {
+    const {
+      field, value, onChange,
+    } = opts
+    return (
+      <FieldInput
+        layout="modal"
+        key={field.name}
+        name={field.name}
+        type={field.type || 'string'}
+        value={value}
+        label={field.label}
+        description={field.description}
+        onChange={onChange}
+        width={ThemeProps.inputSizes.large.width}
+        required={field.required}
+        highlight={Boolean(this.state.highlightFieldNames.find(n => n === field.name))}
+        disabledLoading={this.props.loading}
+      />
+    )
+  }
+
+  renderForm() {
+    const fields = [
+      this.renderField({
+        field: {
+          name: 'host',
+          required: true,
+          label: 'Host',
+          description: 'The Coriolis Snapshot Agent API hostname of the added bare metal server',
+        },
+        value: this.state.host,
+        onChange: host => { this.setState({ host }) },
+      }),
+      this.renderField({
+        field: {
+          name: 'port',
+          required: true,
+          label: 'Port',
+          description: 'The port number used for accessing the Coriolis Snapshot Agent API of the added bare metal server',
+        },
+        value: this.state.port,
+        onChange: port => { this.setState({ port }) },
+      }),
+    ]
+
+    return (
+      <Form>
+        {fields}
+      </Form>
+    )
+  }
+
+  renderButtons() {
+    return (
+      <Buttons>
+        <Button
+          secondary
+          large
+          onClick={this.props.onRequestClose}
+        >Cancel
+        </Button>
+        {this.props.loading ? (
+          <LoadingButton large>{this.props.server ? 'Updating ...' : 'Adding ...'}</LoadingButton>
+        ) : (
+          <Button
+            large
+            onClick={() => { this.handleAddClick() }}
+          >{this.props.server ? 'Update' : 'Add'}
+          </Button>
+        )}
+      </Buttons>
+    )
+  }
+
+  render() {
+    return (
+      <Modal
+        isOpen
+        title={`${this.props.server ? 'Update' : 'Add'} Coriolis Bare Metal Server`}
+        onRequestClose={this.props.onRequestClose}
+      >
+        <Wrapper>
+          <Image />
+          {this.renderForm()}
+          {this.renderButtons()}
+        </Wrapper>
+      </Modal>
+    )
+  }
+}
+
+export default MetalHubModal

+ 60 - 0
src/components/modules/MetalHubModule/MetalHubModal/images/server.svg

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg    xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   id="svg956"
+   version="1.1"
+   viewBox="0 0 25.399999 25.400001"
+   height="96"
+   width="96">
+  <g
+     id="layer1"
+     >
+    <g
+       transform="matrix(0.05207361,0,0,0.05207361,-0.55007587,-0.60983933)"
+       id="g1545">
+      <path
+         fill="#7a8c98"
+         d="M 482.834,445.69221 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 v -124.253 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.916 -5.607,12.522 -12.522,12.522 z"
+         style="fill:#a4aab5;fill-opacity:1"
+         id="path1507" />
+      <path
+         fill="#7a8c98"
+         d="M 482.834,217.24483 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 80.470853 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 V 204.72383 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
+         style="fill:#a4aab5;fill-opacity:1"
+         id="path1513" />
+      <path
+         fill="#9facba"
+         d="M 317.60028,182.09881 H 74.470786 c -14.08057,0 -25.495786,-16.82319 -25.495786,-37.5744 0,-20.75125 11.415216,-37.57443 25.495786,-37.57443 H 317.60028 c 14.08056,0 25.49578,16.82318 25.49578,37.57443 -0.001,20.75121 -11.41522,37.5744 -25.49578,37.5744 z"
+         style="fill:#ffffff;fill-opacity:1;stroke-width:1.3971"
+         id="path1515" />
+      <path
+         fill="#60b7ff"
+         d="m 384.5962,182.08938 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.56897 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.56897 c 0.001,9.543 -7.736,17.28 -17.28,17.28 z"
+         style="fill:#ffffff;fill-opacity:1"
+         id="path1517" />
+      <path
+         fill="#fe646f"
+         d="m 445.745,106.95941 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.56897 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.56897 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
+         style="fill:#ffffff;fill-opacity:1"
+         id="path1537" />
+      <path
+         id="path1515-2"
+         style="fill:#ffffff;fill-opacity:1;stroke-width:1.3971"
+         d="M 315.11203,408.88257 H 71.982511 c -14.080571,0 -25.495788,-16.82319 -25.495788,-37.57444 0,-20.75125 11.415217,-37.57443 25.495788,-37.57443 H 315.11203 c 14.08056,0 25.49579,16.82318 25.49579,37.57443 -9.9e-4,20.75125 -11.41523,37.57444 -25.49579,37.57444 z"
+         fill="#9facba" />
+      <path
+         id="path1517-7"
+         style="fill:#ffffff;fill-opacity:1;stroke-width:1"
+         d="m 390.15768,405.01754 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.56901 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.56901 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
+         fill="#60b7ff" />
+      <path
+         id="path1537-1"
+         style="fill:#ffffff;fill-opacity:1;stroke-width:1"
+         d="m 449.91183,329.11647 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.22699 v 40.56901 c 0,9.18501 7.172,16.67501 16.218,17.22701 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28001 v -40.56898 c 0,-9.54499 -7.737,-17.282 -17.28,-17.28199 z"
+         fill="#fe646f" />
+    </g>
+  </g>
+</svg>

+ 6 - 0
src/components/modules/MetalHubModule/MetalHubModal/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "MetalHubModal",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./MetalHubModal.tsx"
+}

+ 376 - 0
src/components/modules/MetalHubModule/MetalHubServerDetailsContent/MetalHubServerDetailsContent.tsx

@@ -0,0 +1,376 @@
+/*
+Copyright (C) 2022  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 moment from 'moment'
+
+import CopyValue from '@src/components/ui/CopyValue'
+import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
+import Button from '@src/components/ui/Button'
+
+import { ThemePalette, ThemeProps } from '@src/components/Theme'
+import { MetalHubDisk, MetalHubNic, MetalHubServer } from '@src/@types/MetalHub'
+import {
+  ArrowStyled, GlobalStyle, HeaderIcon, HeaderName, Row, RowBody, RowBodyColumn, RowBodyColumnValue, RowHeader, RowHeaderColumn,
+} from '@src/components/modules/TransferModule/TransferDetailsTable'
+import { Collapse } from 'react-collapse'
+import LoadingButton from '@src/components/ui/LoadingButton'
+import StatusPill from '@src/components/ui/StatusComponents/StatusPill'
+
+const Wrapper = styled.div`
+  ${ThemeProps.exactWidth(ThemeProps.contentWidth)}
+  margin: 0 auto;
+  padding-left: 126px;
+`
+const Info = styled.div`
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 32px;
+  margin-left: -32px;
+`
+const Field = styled.div`
+  ${ThemeProps.exactWidth('calc(50% - 32px)')}
+  margin-bottom: 32px;
+  margin-left: 32px;
+`
+const Value = styled.div``
+const Label = styled.div`
+  font-size: 10px;
+  font-weight: ${ThemeProps.fontWeights.medium};
+  color: ${ThemePalette.grayscale[3]};
+  text-transform: uppercase;
+  margin-bottom: 3px;
+`
+const LoadingWrapper = styled.div`
+  display: flex;
+  justify-content: center;
+  width: 100%;
+  margin: 32px 0 64px 0;
+`
+const Buttons = styled.div<any>`
+  margin-top: 64px;
+  display: flex;
+  justify-content: space-between;
+`
+const ButtonsColumn = styled.div<any>`
+  display: flex;
+  flex-direction: column;
+  button {
+    margin-bottom: 16px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+`
+const Table = styled.div``
+const TableBody = styled.div``
+const TableHeader = styled.div`
+  background: ${ThemePalette.grayscale[1]};
+  border-radius: ${ThemeProps.borderRadius};
+  margin-bottom: 32px;
+  &:last-child { margin-bottom: 0; }
+`
+const TableHeaderInfo = styled.div`
+  padding: 16px;
+  border-bottom: 1px solid ${ThemePalette.grayscale[5]};
+  font-size: 16px;
+`
+const TableBodyContent = styled.div`
+  font-size: 14px;
+`
+const HeaderSubtitle = styled.span`
+  color: ${ThemePalette.grayscale[5]};
+  margin-left: 4px;
+`
+type Props = {
+  server: MetalHubServer | null,
+  loading: boolean,
+  creatingReplica: boolean,
+  creatingMigration: boolean,
+  onCreateReplicaClick: () => void,
+  onCreateMigrationClick: () => void,
+  onDeleteClick: () => void,
+}
+type State = {
+  openedRows: string[],
+}
+@observer
+class MetalHubServerDetailsContent extends React.Component<Props, State> {
+  state: State = {
+    openedRows: [],
+  }
+
+  handleRowClick(id: string) {
+    if (this.state.openedRows.find(i => i === id)) {
+      this.setState(prevState => ({
+        openedRows: prevState.openedRows.filter(i => id !== i),
+      }))
+    } else {
+      this.setState(prevState => ({
+        openedRows: [...prevState.openedRows, id],
+      }))
+    }
+  }
+
+  renderLoading() {
+    return (
+      <LoadingWrapper>
+        <StatusImage />
+      </LoadingWrapper>
+    )
+  }
+
+  renderButtons() {
+    if (this.props.loading) {
+      return null
+    }
+
+    const creating = this.props.creatingReplica || this.props.creatingMigration
+
+    return (
+      <Buttons>
+        <ButtonsColumn>
+          {this.props.creatingReplica ? (
+            <LoadingButton>
+              Loading Wizard ...
+            </LoadingButton>
+          ) : (
+            <Button
+              onClick={this.props.onCreateReplicaClick}
+              disabled={creating || !this.props.server?.active}
+              hollow
+            >Create Replica
+            </Button>
+          )}
+          {this.props.creatingMigration ? (
+            <LoadingButton>
+              Loading Wizard ...
+            </LoadingButton>
+          ) : (
+            <Button
+              hollow
+              disabled={creating || !this.props.server?.active}
+              onClick={this.props.onCreateMigrationClick}
+            >Create Migration
+            </Button>
+          )}
+        </ButtonsColumn>
+        <ButtonsColumn>
+          <Button
+            alert
+            hollow
+            onClick={() => { this.props.onDeleteClick() }}
+          >Remove Server
+          </Button>
+        </ButtonsColumn>
+      </Buttons>
+    )
+  }
+
+  renderInfo() {
+    if (this.props.loading || !this.props.server) {
+      return null
+    }
+    const server = this.props.server
+    return (
+      <Info>
+        <Field>
+          <Label>Hostname</Label>
+          {this.renderValue(server.hostname || '-')}
+        </Field>
+        <Field>
+          <Label>ID</Label>
+          {this.renderValue(String(server.id))}
+        </Field>
+        <Field>
+          <Label>Status</Label>
+          <Value>{server.active ? (
+            <StatusPill status="COMPLETED" label="Active" />
+          ) : (
+            <StatusPill status="ERROR" label="Inactive" />
+          )}
+          </Value>
+        </Field>
+        <Field>
+          <Label>API Endpoint</Label>
+          {this.renderValue(String(server.api_endpoint))}
+        </Field>
+        <Field>
+          <Label>Created At</Label>
+          <Value>{moment(server.created_at).format('YYYY-MM-DD HH:mm:ss')}</Value>
+        </Field>
+        <Field>
+          <Label>Updated At</Label>
+          <Value>{moment(server.updated_at).format('YYYY-MM-DD HH:mm:ss')}</Value>
+        </Field>
+        <Field>
+          <Label>Firmware Type</Label>
+          {this.renderValue(server.firmware_type || '-')}
+        </Field>
+        <Field>
+          <Label>CPU Cores</Label>
+          {server.physical_cores || '-'} physical, {server.logical_cores || '-'} logical
+        </Field>
+        <Field>
+          <Label>Memory Size</Label>
+          {server.memory ? this.renderValue(`${(server.memory / 1024 / 1024 / 1024).toFixed(2)} GB`) : '-'}
+        </Field>
+        <Field>
+          <Label>Operating System</Label>
+          {server.os_info.os_name ? this.renderValue(`${server.os_info.os_name} ${server.os_info.os_version}`) : '-'}
+        </Field>
+      </Info>
+    )
+  }
+
+  renderValue(value: string) {
+    return value !== '-' ? (
+      <CopyValue
+        value={value}
+        maxWidth="90%"
+      />
+    ) : <Value>{value}</Value>
+  }
+
+  renderNics(nics: MetalHubNic[]) {
+    return nics.map(nic => {
+      const isOpened: boolean = Boolean(this.state.openedRows.find(i => i === nic.nic_name))
+
+      return (
+        <Row key={nic.nic_name} onClick={() => { this.handleRowClick(nic.nic_name) }}>
+          <ArrowStyled
+            primary
+            orientation={isOpened ? 'up' : 'down'}
+            opacity={isOpened ? 1 : 0}
+            thick
+          />
+          <RowHeader>
+            <RowHeaderColumn>
+              <HeaderIcon icon="network" />
+              <HeaderName source>{nic.nic_name}</HeaderName>
+            </RowHeaderColumn>
+            <RowHeaderColumn />
+          </RowHeader>
+          <Collapse isOpened={isOpened}>
+            <RowBody>
+              <RowBodyColumn>
+                {[
+                  `MAC Address: ${nic.mac_address}`,
+                  `IP Addresses: ${nic.ip_addresses.join(', ')}`,
+                  `Interface Type: ${nic.interface_type}`,
+                ].map(l => <RowBodyColumnValue key={l}>{l}</RowBodyColumnValue>)}
+              </RowBodyColumn>
+            </RowBody>
+          </Collapse>
+        </Row>
+      )
+    })
+  }
+
+  renderPartitions(partitions: MetalHubDisk['partitions'], sectorSize: number) {
+    return partitions.map(partition => {
+      const isOpened: boolean = Boolean(this.state.openedRows.find(i => i === partition.partition_uuid))
+      const size = partition.sectors * sectorSize
+      const sizeString = size < 1024 * 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)} MB` : `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`
+      return (
+        <Row key={partition.partition_uuid} onClick={() => { this.handleRowClick(partition.partition_uuid) }}>
+          <ArrowStyled
+            primary
+            orientation={isOpened ? 'up' : 'down'}
+            opacity={isOpened ? 1 : 0}
+            thick
+          />
+          <RowHeader>
+            <RowHeaderColumn>
+              <HeaderIcon icon="storage" />
+              <HeaderName source>{partition.name} <HeaderSubtitle>{sizeString}</HeaderSubtitle></HeaderName>
+            </RowHeaderColumn>
+            <RowHeaderColumn />
+          </RowHeader>
+          <Collapse isOpened={isOpened}>
+            <RowBody>
+              <RowBodyColumn>
+                {[
+                  `ID: ${partition.partition_uuid}`,
+                  `Path: ${partition.path}`,
+                  `Sectors: ${partition.sectors}`,
+                  `Start Sector: ${partition.start_sector}`,
+                  `End Sector: ${partition.end_sector}`,
+                ].map(l => <RowBodyColumnValue key={l}>{l}</RowBodyColumnValue>)}
+              </RowBodyColumn>
+            </RowBody>
+          </Collapse>
+        </Row>
+      )
+    })
+  }
+
+  renderNicsTable() {
+    if (this.props.loading || !this.props.server?.nics) {
+      return null
+    }
+    return (
+      <Table style={{ marginTop: '24px' }}>
+        <Label>Network Interface Controllers</Label>
+        <TableBody>
+          <TableHeader>
+            <TableBodyContent>
+              {this.renderNics(this.props.server.nics)}
+            </TableBodyContent>
+          </TableHeader>
+        </TableBody>
+      </Table>
+    )
+  }
+
+  renderPartitionsTable() {
+    if (this.props.loading || !this.props.server?.disks) {
+      return null
+    }
+    return (
+      <Table>
+        <GlobalStyle />
+        <Label>Disk Partitions</Label>
+        <TableBody>
+          {this.props.server.disks.map(disk => (
+            <TableHeader key={disk.id}>
+              <TableHeaderInfo>{disk.name} <HeaderSubtitle>{(disk.size / 1024 / 1024 / 1024).toFixed(2)} GB</HeaderSubtitle></TableHeaderInfo>
+              <TableBodyContent>
+                {this.renderPartitions(disk.partitions, disk.physical_sector_size)}
+              </TableBodyContent>
+            </TableHeader>
+          ))}
+        </TableBody>
+      </Table>
+    )
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        {this.renderInfo()}
+        {this.renderPartitionsTable()}
+        {this.renderNicsTable()}
+        {this.props.loading ? this.renderLoading() : null}
+        {this.renderButtons()}
+        <GlobalStyle />
+      </Wrapper>
+    )
+  }
+}
+
+export default MetalHubServerDetailsContent

+ 6 - 0
src/components/modules/MetalHubModule/MetalHubServerDetailsContent/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "MetalHubServerDetailsContent",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./MetalHubServerDetailsContent.tsx"
+}

+ 5 - 1
src/components/modules/NavigationModule/Navigation/Navigation.tsx

@@ -37,6 +37,7 @@ import userImage from './images/user-menu.svg'
 import logsImage from './images/logs-menu.svg'
 import dashboardImage from './images/dashboard-menu.svg'
 import minionPoolsImage from './images/minion-pool-menu.svg'
+import bareMetalServersImage from './images/bare-metal-servers.svg'
 
 const isCollapsed = (props: any) => props.collapsed
   || (window.outerWidth <= ThemeProps.mobileMaxWidth)
@@ -114,7 +115,7 @@ const MenuItem = styled(Link)<{ selected?: boolean | null }>`
   cursor: pointer;
   margin-top: 26px;
   text-decoration: none;
-  width: 145px;
+  width: 160px;
   margin-left: 32px;
 `
 const SmallMenu = styled.div<any>`
@@ -394,6 +395,9 @@ class Navigation extends React.Component<Props> {
               case 'minion-pools':
                 menuImage = minionPoolsImage
                 break
+              case 'bare-metal-servers':
+                menuImage = bareMetalServersImage
+                break
               case 'planning':
                 menuImage = planningImage
                 break

+ 63 - 0
src/components/modules/NavigationModule/Navigation/images/bare-metal-servers.svg

@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   viewBox="0 0 6.3499998 6.3499998"
+   height="24"
+   width="24">
+  <g
+     id="layer1"
+     >
+    <g
+       transform="translate(-25.161605,2.3831214)"
+       id="g1582">
+      <g
+         transform="matrix(0.0122557,0,0,0.0122557,25.199146,-2.3366267)"
+         id="g1545">
+        <path
+           fill="#7a8c98"
+           d="M 482.834,445.69221 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 v -124.253 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.916 -5.607,12.522 -12.522,12.522 z"
+           style="fill:#ffffff;fill-opacity:1"
+           id="path1507" />
+        <path
+           fill="#7a8c98"
+           d="M 482.834,217.24483 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 80.470853 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 V 204.72383 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
+           style="fill:#ffffff;fill-opacity:1"
+           id="path1513" />
+        <path
+           fill="#9facba"
+           d="M 317.60028,182.09881 H 74.470786 c -14.08057,0 -25.495786,-16.82319 -25.495786,-37.5744 0,-20.75125 11.415216,-37.57443 25.495786,-37.57443 H 317.60028 c 14.08056,0 25.49578,16.82318 25.49578,37.57443 -0.001,20.75121 -11.41522,37.5744 -25.49578,37.5744 z"
+           style="fill:#000008;fill-opacity:1;stroke-width:1.3971"
+           id="path1515" />
+        <path
+           fill="#60b7ff"
+           d="m 384.5962,182.08938 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.56897 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.56897 c 0.001,9.543 -7.736,17.28 -17.28,17.28 z"
+           style="fill:#000000;fill-opacity:1"
+           id="path1517" />
+        <path
+           fill="#fe646f"
+           d="m 445.745,106.95941 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.56897 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.56897 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
+           style="fill:#000000;fill-opacity:1"
+           id="path1537" />
+        <path
+           id="path1515-2"
+           style="fill:#000008;fill-opacity:1;stroke-width:1.3971"
+           d="M 315.11203,408.88257 H 71.982511 c -14.080571,0 -25.495788,-16.82319 -25.495788,-37.57444 0,-20.75125 11.415217,-37.57443 25.495788,-37.57443 H 315.11203 c 14.08056,0 25.49579,16.82318 25.49579,37.57443 -9.9e-4,20.75125 -11.41523,37.57444 -25.49579,37.57444 z"
+           fill="#9facba" />
+        <path
+           id="path1517-7"
+           style="fill:#000000;fill-opacity:1;stroke-width:1"
+           d="m 390.15768,405.01754 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.56901 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.56901 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
+           fill="#60b7ff" />
+        <path
+           id="path1537-1"
+           style="fill:#000000;fill-opacity:1;stroke-width:1"
+           d="m 449.91183,329.11647 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.22699 v 40.56901 c 0,9.18501 7.172,16.67501 16.218,17.22701 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28001 v -40.56898 c 0,-9.54499 -7.737,-17.282 -17.28,-17.28199 z"
+           fill="#fe646f" />
+      </g>
+    </g>
+  </g>
+</svg>

+ 11 - 11
src/components/modules/TransferModule/TransferDetailsTable/TransferDetailsTable.tsx

@@ -34,7 +34,7 @@ import networkIcon from './images/network.svg'
 import storageIcon from './images/storage.svg'
 import arrowIcon from './images/arrow.svg'
 
-const GlobalStyle = createGlobalStyle`
+export const GlobalStyle = createGlobalStyle`
   .ReactCollapse--collapse {
     transition: height 0.4s ease-in-out;
   }
@@ -42,7 +42,7 @@ const GlobalStyle = createGlobalStyle`
 const Wrapper = styled.div<any>`
   margin: 24px 0;
 `
-const ArrowStyled = styled(Arrow)`
+export const ArrowStyled = styled(Arrow)`
   position: absolute;
   left: -24px;
 `
@@ -72,7 +72,7 @@ const InstanceName = styled.div<any>`
 const InstanceBody = styled.div<any>`
   font-size: 14px;
 `
-const Row = styled.div<any>`
+export const Row = styled.div`
   position: relative;
   padding: 8px 0;
   border-bottom: 1px solid white;
@@ -90,29 +90,29 @@ const Row = styled.div<any>`
   }
   cursor: pointer;
 `
-const RowHeader = styled.div<any>`
+export const RowHeader = styled.div`
   display: flex;
   align-items: center;
   padding: 0 16px;
 `
-const RowHeaderColumn = styled.div<any>`
+export const RowHeaderColumn = styled.div`
   display: flex;
   align-items: center;
   ${ThemeProps.exactWidth('50%')}
   &:last-child { margin-left: 19px; }
 `
-const HeaderName = styled.div<any>`
+export const HeaderName = styled.div<{ source?: boolean }>`
   overflow: hidden;
   text-overflow: ellipsis;
   ${props => ThemeProps.exactWidth(`calc(100% - ${props.source ? 120 : 8}px)`)}
 `
-const RowBody = styled.div<any>`
+export const RowBody = styled.div`
   display: flex;
   color: ${ThemePalette.grayscale[5]};
   padding: 0 16px;
   margin-top: 4px;
 `
-const RowBodyColumn = styled.div<any>`
+export const RowBodyColumn = styled.div`
   &:first-child {
     ${ThemeProps.exactWidth('calc(50% - 70px)')}
     margin-right: 88px;
@@ -121,7 +121,7 @@ const RowBodyColumn = styled.div<any>`
     ${ThemeProps.exactWidth('calc(50% - 16px)')}
   }
 `
-const RowBodyColumnValue = styled.div<any>`
+export const RowBodyColumnValue = styled.div`
   overflow-wrap: break-word;
 `
 const getHeaderIcon = (icon: 'instance' | 'network' | 'storage'): string => {
@@ -134,13 +134,13 @@ const getHeaderIcon = (icon: 'instance' | 'network' | 'storage'): string => {
       return storageIcon
   }
 }
-const HeaderIcon = styled.div<any>`
+export const HeaderIcon = styled.div<{ icon: 'instance' | 'network' | 'storage' }>`
   width: 16px;
   height: 16px;
   background: url('${props => getHeaderIcon(props.icon)}') center no-repeat;
   margin-right: 16px;
 `
-const ArrowIcon = styled.div<any>`
+export const ArrowIcon = styled.div`
   width: 32px;
   height: 16px;
   background: url('${arrowIcon}') center no-repeat;

+ 3 - 2
src/components/modules/WizardModule/WizardOptions/WizardOptions.tsx

@@ -182,6 +182,7 @@ type Props = {
   selectedInstances?: Instance[] | null,
   showSeparatePerVm?: boolean
   data?: { [prop: string]: any } | null,
+  executeNowOptionsDisabled?: boolean,
   getFieldValue?: (
     fieldName: string,
     defaultValue: any,
@@ -298,8 +299,8 @@ class WizardOptions extends React.Component<Props> {
         name: 'execute_now_options',
         type: 'object',
         properties: executionOptions,
-        disabled: !executeNowValue,
-        description: !executeNowValue ? 'Enable \'Execute Now\' to set \'Execute Now Options\'' : `Set the options for ${this.props.wizardType} execution`,
+        disabled: !executeNowValue || this.props.executeNowOptionsDisabled,
+        description: this.props.executeNowOptionsDisabled ? 'The \'Execute Now Options\' are disabled for the source provider' : !executeNowValue ? 'Enable \'Execute Now\' to set \'Execute Now Options\'' : `Set the options for ${this.props.wizardType} execution`,
       })
     } else if (this.props.wizardType === 'migration' || this.props.wizardType === 'migration-destination-options-edit') {
       fieldsSchema = [...fieldsSchema, ...migrationFields]

+ 1 - 0
src/components/modules/WizardModule/WizardPageContent/WizardPageContent.tsx

@@ -460,6 +460,7 @@ class WizardPageContent extends React.Component<Props, State> {
             }}
             layout="page"
             dictionaryKey={`${this.props.wizardData.target ? this.props.wizardData.target.type : ''}-destination`}
+            executeNowOptionsDisabled={!this.props.providerStore.hasExecuteNowOptions(this.props.wizardData.source!.type)}
           />
         )
         break

+ 234 - 0
src/components/smart/MetalHubServerDetailsPage/MetalHubServerDetailsPage.tsx

@@ -0,0 +1,234 @@
+/*
+Copyright (C) 2022  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 DetailsTemplate from '@src/components/modules/TemplateModule/DetailsTemplate'
+import DetailsPageHeader from '@src/components/modules/DetailsModule/DetailsPageHeader'
+import DetailsContentHeader from '@src/components/modules/DetailsModule/DetailsContentHeader'
+import AlertModal from '@src/components/ui/AlertModal'
+
+import { ThemePalette } from '@src/components/Theme'
+import type { WizardData } from '@src/@types/WizardData'
+
+import metalHubStore from '@src/stores/MetalHubStore'
+import userStore from '@src/stores/UserStore'
+import MetalHubServerDetailsContent from '@src/components/modules/MetalHubModule/MetalHubServerDetailsContent'
+import instanceSource from '@src/sources/InstanceSource'
+import notificationStore from '@src/stores/NotificationStore'
+import { wizardPages } from '@src/constants'
+import { DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
+import instanceStore from '@src/stores/InstanceStore'
+import MetalHubModal from '@src/components/modules/MetalHubModule/MetalHubModal'
+import serverImage from './images/server.svg'
+
+const Wrapper = styled.div<any>``
+
+type Props = {
+  match: { params: { id: string } },
+  history: any,
+}
+type State = {
+  showDeleteServerAlert: boolean,
+  creatingReplica: boolean,
+  creatingMigration: boolean,
+  showEditServerModal: boolean,
+  updatingServer: boolean,
+}
+@observer
+class MetalHubServerDetailsPage extends React.Component<Props, State> {
+  state: State = {
+    showDeleteServerAlert: false,
+    creatingReplica: false,
+    creatingMigration: false,
+    showEditServerModal: false,
+    updatingServer: false,
+  }
+
+  componentDidMount() {
+    document.title = 'Bare Metal Server Details'
+
+    this.loadData()
+  }
+
+  componentWillUnmount() {
+    metalHubStore.clearServerDetails()
+  }
+
+  handleUserItemClick(item: { value: string }) {
+    switch (item.value) {
+      case 'signout':
+        userStore.logout()
+        break
+      default:
+    }
+  }
+
+  async handleDeleteConfirmation() {
+    this.setState({ showDeleteServerAlert: false })
+
+    await metalHubStore.deleteServer(metalHubStore.serverDetails!.id)
+    this.props.history.push('/bare-metal-servers')
+  }
+
+  handleRefresh() {
+    metalHubStore.refreshServer(metalHubStore.serverDetails!.id)
+  }
+
+  handleDeleteServerClick() {
+    this.setState({ showDeleteServerAlert: true })
+  }
+
+  async loadData() {
+    const serverId = Number(this.props.match.params.id)
+    await metalHubStore.getServerDetails(serverId)
+    if (!metalHubStore.serverDetails) {
+      this.props.history.push('/bare-metal-servers')
+    }
+  }
+
+  async handleCreate(type: 'replica' | 'migration') {
+    const endpoint = await metalHubStore.getMetalHubEndpoint()
+
+    // Remove the instances for cache so that the wizard has the latest data before it's shown
+    instanceSource.removeInstancesFromCache(endpoint.id)
+    await instanceStore.loadInstancesInChunks({ endpoint, vmsPerPage: Infinity })
+    const instance = instanceStore.backgroundInstances.find(i => String(i.id) === String(metalHubStore.serverDetails?.id))
+    if (!instance) {
+      notificationStore.alert(`Could not find instance ID ${metalHubStore.serverDetails?.id} on endpoint '${endpoint.name}'`, 'error')
+      throw new Error('Instance not found')
+    }
+    const data: WizardData = {
+      source: endpoint,
+      selectedInstances: [instance],
+    }
+    this.props.history.push(`/wizard/${type}/?d=${window.btoa(JSON.stringify({
+      data,
+      currentPage: wizardPages.find(p => p.id === 'target'),
+    }))}`)
+  }
+
+  async handleCreateReplicaClick() {
+    this.setState({ creatingReplica: true })
+    try {
+      await this.handleCreate('replica')
+    } catch (err) {
+      this.setState({ creatingReplica: false })
+    }
+  }
+
+  async handleCreateMigrationClick() {
+    this.setState({ creatingMigration: true })
+    try {
+      await this.handleCreate('migration')
+    } catch (err) {
+      this.setState({ creatingMigration: false })
+    }
+  }
+
+  async handleEditServer(apiEndpoint: string) {
+    this.setState({ updatingServer: true })
+    await metalHubStore.patchServer(metalHubStore.serverDetails!.id, apiEndpoint)
+    await this.loadData()
+    this.setState({ showEditServerModal: false, updatingServer: false })
+  }
+
+  render() {
+    const creating = this.state.creatingReplica || this.state.creatingMigration
+    const dropdownActions: DropdownAction[] = [
+      {
+        label: 'Create Replica',
+        action: () => { this.handleCreateReplicaClick() },
+        color: ThemePalette.primary,
+        disabled: creating || !metalHubStore.serverDetails?.active,
+      },
+      {
+        label: 'Create Migration',
+        color: ThemePalette.primary,
+        action: () => { this.handleCreateMigrationClick() },
+        disabled: creating || !metalHubStore.serverDetails?.active,
+      },
+      {
+        label: 'Edit',
+        action: () => { this.setState({ showEditServerModal: true }) },
+        disabled: creating || !metalHubStore.serverDetails,
+      },
+      {
+        label: 'Refresh',
+        action: () => { this.handleRefresh() },
+        disabled: creating || !metalHubStore.serverDetails,
+      },
+      {
+        label: 'Remove Server',
+        color: ThemePalette.alert,
+        action: () => { this.handleDeleteServerClick() },
+      },
+    ]
+
+    return (
+      <Wrapper>
+        <DetailsTemplate
+          pageHeaderComponent={(
+            <DetailsPageHeader
+              user={userStore.loggedUser}
+              onUserItemClick={item => { this.handleUserItemClick(item) }}
+            />
+          )}
+          contentHeaderComponent={(
+            <DetailsContentHeader
+              itemTitle={metalHubStore.serverDetails?.hostname || 'No Hostname'}
+              itemType="server"
+              backLink="/bare-metal-servers"
+              dropdownActions={dropdownActions}
+              typeImage={serverImage}
+            />
+          )}
+          contentComponent={(
+            <MetalHubServerDetailsContent
+              server={metalHubStore.serverDetails}
+              loading={metalHubStore.loadingServerDetails || metalHubStore.refreshingServer}
+              creatingReplica={this.state.creatingReplica}
+              creatingMigration={this.state.creatingMigration}
+              onCreateReplicaClick={() => { this.handleCreateReplicaClick() }}
+              onCreateMigrationClick={() => { this.handleCreateMigrationClick() }}
+              onDeleteClick={() => this.handleDeleteServerClick()}
+            />
+          )}
+        />
+        {this.state.showDeleteServerAlert ? (
+          <AlertModal
+            isOpen
+            title="Remove Bare Metal Server?"
+            message="Are you sure you want to remove this server?"
+            extraMessage="By removing a server from the hub, Coriolis will not be able to migrate it or do any other replica executions of it."
+            onConfirmation={() => { this.handleDeleteConfirmation() }}
+            onRequestClose={() => { this.setState({ showDeleteServerAlert: false }) }}
+          />
+        ) : null}
+        {this.state.showEditServerModal ? (
+          <MetalHubModal
+            loading={this.state.updatingServer}
+            server={metalHubStore.serverDetails!}
+            onEditClick={e => { this.handleEditServer(e) }}
+            onRequestClose={() => { this.setState({ showEditServerModal: false }) }}
+          />
+        ) : null}
+      </Wrapper>
+    )
+  }
+}
+
+export default MetalHubServerDetailsPage

+ 105 - 0
src/components/smart/MetalHubServerDetailsPage/images/server.svg

@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg    xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   id="svg1067"
+   version="1.1"
+   viewBox="0 0 16.933333 16.933333"
+   height="64"
+   width="64">
+  <defs
+     id="defs1061">
+    <path
+       d="M24,48 C37.254834,48 48,37.254834 48,24 C48,10.745166 37.254834,0 24,0 C10.745166,0 0,10.745166 0,24 C0,37.254834 10.745166,48 24,48 Z"
+       id="path-1" />
+    <linearGradient
+       osb:paint="solid"
+       id="linearGradient4417">
+      <stop
+         id="stop4415"
+         offset="0"
+         style="stop-color:#0044cb;stop-opacity:1;" />
+    </linearGradient>
+    <path
+       d="M24,48 C37.254834,48 48,37.254834 48,24 C48,10.745166 37.254834,0 24,0 C10.745166,0 0,10.745166 0,24 C0,37.254834 10.745166,48 24,48 Z"
+       id="path-1-1" />
+  </defs>
+  <g
+     id="layer1"
+     >
+    <g
+       transform="scale(0.35277778)"
+       style="fill:none;fill-rule:evenodd;stroke:none;stroke-width:1"
+       id="Icon/Project/ProjectListItem">
+      <mask
+         fill="#ffffff"
+         id="mask-2">
+        <use
+           height="100%"
+           width="100%"
+           y="0"
+           x="0"
+           id="use838"
+           xlink:href="#path-1-1" />
+      </mask>
+      <use
+         style="fill:#ffffff;fill-opacity:1"
+         height="100%"
+         width="100%"
+         y="0"
+         x="0"
+         xlink:href="#path-1-1"
+         fill-rule="evenodd"
+         fill="#c8ccd7"
+         id="Pat-Benetar" />
+      <g
+         transform="matrix(0.06333812,0,0,0.06333812,7.7854416,7.7334707)"
+         id="g1545">
+        <path
+           fill="#7a8c98"
+           d="M 482.834,445.69221 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 v -124.253 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.916 -5.607,12.522 -12.522,12.522 z"
+           style="fill:#0044ca;fill-opacity:1"
+           id="path1507" />
+        <path
+           fill="#7a8c98"
+           d="M 482.834,217.24483 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 80.470853 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 V 204.72383 c -10e-4,6.915 -5.607,12.521 -12.522,12.521 z"
+           style="fill:#0044ca;fill-opacity:1"
+           id="path1513" />
+        <path
+           fill="#9facba"
+           d="M 317.60028,182.09881 H 74.470786 c -14.08057,0 -25.495786,-16.82319 -25.495786,-37.5744 0,-20.75125 11.415216,-37.57443 25.495786,-37.57443 H 317.60028 c 14.08056,0 25.49578,16.82318 25.49578,37.57443 -0.001,20.75121 -11.41522,37.5744 -25.49578,37.5744 z"
+           style="fill:#ffffff;fill-opacity:1;stroke-width:1.3971"
+           id="path1515" />
+        <path
+           fill="#60b7ff"
+           d="m 384.5962,182.08938 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.56897 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.56897 c 0.001,9.543 -7.736,17.28 -17.28,17.28 z"
+           style="fill:#ffffff;fill-opacity:1"
+           id="path1517" />
+        <path
+           fill="#fe646f"
+           d="m 445.745,106.95941 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.227 v 40.56897 c 0,9.185 7.172,16.675 16.218,17.227 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28 v -40.56897 c 0,-9.545 -7.737,-17.282 -17.28,-17.282 z"
+           style="fill:#ffffff;fill-opacity:1"
+           id="path1537" />
+        <path
+           id="path1515-2"
+           style="fill:#ffffff;fill-opacity:1;stroke-width:1.3971"
+           d="M 315.11203,408.88257 H 71.982511 c -14.080571,0 -25.495788,-16.82319 -25.495788,-37.57444 0,-20.75125 11.415217,-37.57443 25.495788,-37.57443 H 315.11203 c 14.08056,0 25.49579,16.82318 25.49579,37.57443 -9.9e-4,20.75125 -11.41523,37.57444 -25.49579,37.57444 z"
+           fill="#9facba" />
+        <path
+           id="path1517-7"
+           style="fill:#ffffff;fill-opacity:1;stroke-width:1"
+           d="m 390.15768,405.01754 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.56901 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.56901 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
+           fill="#60b7ff" />
+        <path
+           id="path1537-1"
+           style="fill:#ffffff;fill-opacity:1;stroke-width:1"
+           d="m 449.91183,329.11647 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.22699 v 40.56901 c 0,9.18501 7.172,16.67501 16.218,17.22701 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28001 v -40.56898 c 0,-9.54499 -7.737,-17.282 -17.28,-17.28199 z"
+           fill="#fe646f" />
+      </g>
+    </g>
+  </g>
+</svg>

+ 6 - 0
src/components/smart/MetalHubServerDetailsPage/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "MetalHubServerDetailsPage",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./MetalHubServerDetailsPage.tsx"
+}

+ 195 - 0
src/components/smart/MetalHubServersPage/MetalHubServersPage.tsx

@@ -0,0 +1,195 @@
+/*
+Copyright (C) 2022  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 MainTemplate from '@src/components/modules/TemplateModule/MainTemplate'
+import Navigation from '@src/components/modules/NavigationModule/Navigation'
+import FilterList from '@src/components/ui/Lists/FilterList'
+import PageHeader from '@src/components/smart/PageHeader'
+import configLoader from '@src/utils/Config'
+import metalHubStore from '@src/stores/MetalHubStore'
+import { MetalHubServer } from '@src/@types/MetalHub'
+import MetalHubServerListItem from '@src/components/modules/MetalHubModule/MetalHubListItem'
+
+import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
+import MetalHubListHeader from '@src/components/modules/MetalHubModule/MetalHubListHeader'
+import projectStore from '@src/stores/ProjectStore'
+import MetalHubModal from '@src/components/modules/MetalHubModule/MetalHubModal'
+import emptyListImage from './images/server.svg'
+
+const Wrapper = styled.div``
+const ErrorWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  height: 100%;
+  justify-content: center;
+`
+const ErrorMessage = styled.div`
+  margin-top: 24px;
+  text-align: center;
+  max-width: 600px;
+`
+
+type State = {
+  modalIsOpen: boolean,
+  showNewServerModal: boolean,
+}
+@observer
+class MetalHubServersPage extends React.Component<{ history: any }, State> {
+  state = {
+    modalIsOpen: false,
+    showNewServerModal: false,
+  }
+
+  pollTimeout: number = 0
+
+  stopPolling: boolean = false
+
+  componentDidMount() {
+    document.title = 'Bare Metal Servers'
+
+    metalHubStore.loadFingerprint()
+    projectStore.getProjects()
+
+    this.stopPolling = false
+    this.pollData(true)
+  }
+
+  componentWillUnmount() {
+    clearTimeout(this.pollTimeout)
+    this.stopPolling = true
+  }
+
+  handleModalOpen() {
+    this.setState({ modalIsOpen: true })
+  }
+
+  handleModalClose() {
+    this.setState({ modalIsOpen: false }, () => {
+      this.pollData()
+    })
+  }
+
+  handleReloadButtonClick() {
+    metalHubStore.loadFingerprint()
+    metalHubStore.getServers({ showLoading: true })
+  }
+
+  handleEmptyListButtonClick() {
+    this.setState({ showNewServerModal: true, modalIsOpen: true })
+  }
+
+  async handleNewServer(endpoint: string) {
+    await metalHubStore.addServer(endpoint)
+    this.setState({ showNewServerModal: false, modalIsOpen: false })
+    await metalHubStore.getServers()
+  }
+
+  handleProjectChange() {
+    metalHubStore.getServers({ showLoading: true })
+  }
+
+  async pollData(showLoading?: boolean) {
+    if (this.state.modalIsOpen || this.stopPolling) {
+      return
+    }
+
+    await metalHubStore.getServers({ showLoading, skipLog: true })
+    this.pollTimeout = window.setTimeout(() => { this.pollData() }, configLoader.config.requestPollTimeout)
+  }
+
+  itemFilterFunction(item: MetalHubServer, status?: string | null, filterText?: string): boolean {
+    const usabledFilterText = filterText?.toLowerCase() || ''
+
+    const searchableFields: Array<keyof MetalHubServer> = ['hostname', 'api_endpoint']
+    const filterCount = searchableFields.reduce((acc, key) => {
+      if (!(item[key] as string)?.toLowerCase().includes(usabledFilterText)) {
+        return acc + 1
+      }
+      return acc
+    }, 0)
+    let statusFilter = true
+    if (status !== 'all') {
+      statusFilter = status === 'active' ? item.active : !item.active
+    }
+    return statusFilter && filterCount < searchableFields.length
+  }
+
+  renderEmptyListComponent() {
+    return metalHubStore.loadingServersError ? (
+      <ErrorWrapper>
+        <StatusImage status="ERROR" />
+        <ErrorMessage>Request failed with:<br />{metalHubStore.loadingServersError}</ErrorMessage>
+      </ErrorWrapper>
+    ) : null
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        <MainTemplate
+          navigationComponent={<Navigation currentPage="bare-metal-servers" />}
+          listNoMargin
+          listComponent={(
+            <FilterList
+              filterItems={[{ label: 'All', value: 'all' }, { label: 'Active', value: 'active' }, { label: 'Inactive', value: 'inactive' }]}
+              selectionLabel=""
+              loading={metalHubStore.loadingServers}
+              items={metalHubStore.servers}
+              listHeaderComponent={(
+                <MetalHubListHeader
+                  fingerprint={metalHubStore.fingerprint}
+                  error={metalHubStore.loadingFingerprintError}
+                  hideButton={metalHubStore.servers.length === 0}
+                  onCreateClick={() => { this.setState({ showNewServerModal: true, modalIsOpen: true }) }}
+                />
+              )}
+              onItemClick={(server: MetalHubServer) => { this.props.history.push(`/bare-metal-servers/${server.id}`) }}
+              onReloadButtonClick={() => { this.handleReloadButtonClick() }}
+              itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
+              renderItemComponent={component => <MetalHubServerListItem {...component} />}
+              emptyListImage={emptyListImage}
+              emptyListComponent={this.renderEmptyListComponent()}
+              emptyListMessage="It seems like you don't have any Bare Metal servers in this Hub."
+              emptyListExtraMessage="A Bare Metal server is a virtual machine that is connected to your Coriolis Bare Metal Hub endpoint."
+              emptyListButtonLabel="Add a Bare Metal server"
+              onEmptyListButtonClick={() => { this.handleEmptyListButtonClick() }}
+            />
+          )}
+          headerComponent={(
+            <PageHeader
+              title="Coriolis Bare Metal Servers"
+              onModalOpen={() => { this.handleModalOpen() }}
+              onModalClose={() => { this.handleModalClose() }}
+              onProjectChange={() => { this.handleProjectChange() }}
+            />
+          )}
+        />
+        {this.state.showNewServerModal ? (
+          <MetalHubModal
+            loading={metalHubStore.loadingNewServer}
+            onAddClick={e => { this.handleNewServer(e) }}
+            onRequestClose={() => { this.setState({ showNewServerModal: false, modalIsOpen: false }) }}
+          />
+        ) : null}
+      </Wrapper>
+    )
+  }
+}
+
+export default MetalHubServersPage

+ 75 - 0
src/components/smart/MetalHubServersPage/images/server.svg

@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg    xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   id="svg8"
+   version="1.1"
+   viewBox="0 0 25.399999 25.400001"
+   height="96"
+   width="96">
+  <defs
+     id="defs2">
+    <path
+       d="M 24,48 C 37.254834,48 48,37.254834 48,24 48,10.745166 37.254834,0 24,0 10.745166,0 0,10.745166 0,24 0,37.254834 10.745166,48 24,48 Z"
+       id="path-1" />
+    <linearGradient
+       osb:paint="solid"
+       id="linearGradient4417">
+      <stop
+         id="stop4415"
+         offset="0"
+         style="stop-color:#0044cb;stop-opacity:1;" />
+    </linearGradient>
+  </defs>
+  <g
+     id="layer1"
+     >
+    <g
+       transform="matrix(0.05212894,0,0,0.05212894,-0.67335766,-0.4805101)"
+       id="g2253">
+      <path
+         id="path1507"
+         style="fill:none;fill-opacity:1;stroke:#0000ef;stroke-width:5.07556;stroke-miterlimit:4;stroke-dasharray:20.3022, 20.3022;stroke-dashoffset:41.112;stroke-opacity:1"
+         d="M 482.834,445.69221 H 29.166 c -6.915,0 -12.522,-5.606 -12.522,-12.522 v -124.253 c 0,-6.915 5.606,-12.522 12.522,-12.522 h 453.668 c 6.915,0 12.522,5.606 12.522,12.522 v 124.253 c -10e-4,6.916 -5.607,12.522 -12.522,12.522 z"
+         fill="#7a8c98" />
+      <path
+         fill="#9facba"
+         d="M 315.11203,408.88257 H 71.982511 c -14.080571,0 -25.495788,-16.82319 -25.495788,-37.57444 0,-20.75125 11.415217,-37.57443 25.495788,-37.57443 H 315.11203 c 14.08056,0 25.49579,16.82318 25.49579,37.57443 -9.9e-4,20.75125 -11.41523,37.57444 -25.49579,37.57444 z"
+         style="fill:#ffffff;fill-opacity:1;stroke:#0000ef;stroke-width:5.07556;stroke-miterlimit:4;stroke-dasharray:20.3022, 20.3022;stroke-dashoffset:0;stroke-opacity:1"
+         id="path1515-2" />
+      <path
+         fill="#60b7ff"
+         d="m 395.59578,406.83024 c -9.544,0 -17.28,-7.737 -17.28,-17.28 v -40.56901 c 0,-9.544 7.737,-17.28 17.28,-17.28 9.544,0 17.28,7.737 17.28,17.28 v 40.56901 c 10e-4,9.543 -7.736,17.28 -17.28,17.28 z"
+         style="fill:#ffffff;fill-opacity:1;stroke:#0000ef;stroke-width:5.07556;stroke-miterlimit:4;stroke-dasharray:15.2267, 15.2267;stroke-dashoffset:0;stroke-opacity:1"
+         id="path1517-7" />
+      <path
+         fill="#fe646f"
+         d="m 449.91183,329.11647 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042 -16.218,17.22699 v 40.56901 c 0,9.18501 7.172,16.67501 16.218,17.22701 0.353,0.021 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28001 v -40.56898 c 0,-9.54499 -7.737,-17.282 -17.28,-17.28199 z"
+         style="fill:#ffffff;fill-opacity:1;stroke:#0000ef;stroke-width:5.07556;stroke-miterlimit:4;stroke-dasharray:15.2267, 15.2267;stroke-dashoffset:0;stroke-opacity:1"
+         id="path1537-1" />
+      <path
+         fill="#7a8c98"
+         d="M 483.92165,209.29316 H 30.253641 c -6.915,0 -12.522,-5.606 -12.522,-12.522 V 72.518156 c 0,-6.915 5.606,-12.522 12.522,-12.522 H 483.92165 c 6.915,0 12.522,5.606 12.522,12.522 V 196.77116 c -10e-4,6.916 -5.607,12.522 -12.522,12.522 z"
+         style="fill:none;fill-opacity:1;stroke:#0000ef;stroke-width:5.07556;stroke-miterlimit:4;stroke-dasharray:20.3022, 20.3022;stroke-dashoffset:41.112;stroke-opacity:1"
+         id="path1507-9" />
+      <path
+         id="path1515-2-5"
+         style="fill:#ffffff;fill-opacity:1;stroke:#0000ef;stroke-width:5.07556;stroke-miterlimit:4;stroke-dasharray:20.3022, 20.3022;stroke-dashoffset:0;stroke-opacity:1"
+         d="M 316.19968,172.48352 H 73.070151 c -14.08057,0 -25.49579,-16.82319 -25.49579,-37.57444 0,-20.75125 11.41522,-37.574434 25.49579,-37.574434 H 316.19968 c 14.08056,0 25.49578,16.823184 25.49578,37.574434 -9.8e-4,20.75125 -11.41522,37.57444 -25.49578,37.57444 z"
+         fill="#9facba" />
+      <path
+         id="path1517-7-8"
+         style="fill:#ffffff;fill-opacity:1;stroke:#0000ef;stroke-width:5.07556;stroke-miterlimit:4;stroke-dasharray:15.2267, 15.2267;stroke-dashoffset:0;stroke-opacity:1"
+         d="m 396.68343,170.43119 c -9.544,0 -17.28,-7.737 -17.28,-17.28001 v -40.569 c 0,-9.544 7.737,-17.280004 17.28,-17.280004 9.544,0 17.28,7.737004 17.28,17.280004 v 40.569 c 10e-4,9.54301 -7.736,17.28001 -17.28,17.28001 z"
+         fill="#60b7ff" />
+      <path
+         id="path1537-1-3"
+         style="fill:#ffffff;fill-opacity:1;stroke:#0000ef;stroke-width:5.07556;stroke-miterlimit:4;stroke-dasharray:15.2267, 15.2267;stroke-dashoffset:0;stroke-opacity:1"
+         d="m 450.99948,92.717416 c -0.359,0 -0.709,0.032 -1.062,0.054 -9.046,0.552 -16.218,8.042004 -16.218,17.226994 v 40.56901 c 0,9.18501 7.172,16.67501 16.218,17.22701 0.353,0.0208 0.704,0.054 1.062,0.054 9.544,0 17.28,-7.737 17.28,-17.28001 v -40.56898 c 0,-9.54499 -7.737,-17.282004 -17.28,-17.281994 z"
+         fill="#fe646f" />
+    </g>
+  </g>
+</svg>

+ 6 - 0
src/components/smart/MetalHubServersPage/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "MetalHubServersPage",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./MetalHubServersPage.tsx"
+}

+ 1 - 1
src/components/smart/MinionPoolDetailsPage/MinionPoolDetailsPage.tsx

@@ -22,7 +22,7 @@ import DetailsContentHeader from '@src/components/modules/DetailsModule/DetailsC
 import Modal from '@src/components/ui/Modal'
 import AlertModal from '@src/components/ui/AlertModal'
 
-import type { Action as DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
+import type { DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
 
 import userStore from '@src/stores/UserStore'
 import endpointStore from '@src/stores/EndpointStore'

+ 1 - 1
src/components/smart/MinionPoolsPage/MinionPoolsPage.tsx

@@ -24,7 +24,7 @@ import Navigation from '@src/components/modules/NavigationModule/Navigation'
 import FilterList from '@src/components/ui/Lists/FilterList'
 import PageHeader from '@src/components/smart/PageHeader'
 
-import type { Action as DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
+import type { DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
 
 import projectStore from '@src/stores/ProjectStore'
 

+ 1 - 1
src/components/smart/PageHeader/PageHeader.tsx

@@ -283,7 +283,7 @@ class PageHeader extends React.Component<Props, State> {
 
   async handleProjectChange(project: Project) {
     await userStore.switchProject(project.id)
-    projectStore.getProjects()
+    await projectStore.getProjects()
     notificationStore.loadData(true)
 
     if (this.props.onProjectChange) {

+ 1 - 1
src/components/smart/ReplicaDetailsPage/ReplicaDetailsPage.tsx

@@ -31,7 +31,7 @@ import type { InstanceScript } from '@src/@types/Instance'
 import type { Execution } from '@src/@types/Execution'
 import type { Schedule } from '@src/@types/Schedule'
 import type { Field } from '@src/@types/Field'
-import type { Action as DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
+import type { DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
 
 import replicaStore from '@src/stores/ReplicaStore'
 import migrationStore from '@src/stores/MigrationStore'

+ 1 - 1
src/components/smart/ReplicasPage/ReplicasPage.tsx

@@ -26,7 +26,7 @@ import ReplicaExecutionOptions from '@src/components/modules/TransferModule/Repl
 import ReplicaMigrationOptions from '@src/components/modules/TransferModule/ReplicaMigrationOptions'
 import DeleteReplicaModal from '@src/components/modules/TransferModule/DeleteReplicaModal'
 
-import type { Action as DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
+import type { DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
 import type { Field } from '@src/@types/Field'
 import type { InstanceScript } from '@src/@types/Instance'
 

+ 12 - 0
src/components/smart/WizardPage/WizardPage.tsx

@@ -441,6 +441,9 @@ class WizardPage extends React.Component<Props, State> {
     if (type === 'migration' || type === 'replica') {
       this.setState({ type })
     }
+    if (wizardStore.currentPage.id !== wizardPages[0].id) {
+      this.loadDataForPage(wizardStore.currentPage)
+    }
   }
 
   async loadExtraOptions(field: Field | null, type: 'source' | 'destination', useCache: boolean = true) {
@@ -520,6 +523,15 @@ class WizardPage extends React.Component<Props, State> {
         break
       }
       case 'target': {
+        // If page was set from 'Permalink' directly to 'Target', we may not have the needed data already loaded
+        providerStore.loadProviders()
+        if (!minionPoolStore.minionPools.length) {
+          minionPoolStore.loadMinionPools()
+        }
+        if (!endpointStore.endpoints.length) {
+          endpointStore.getEndpoints()
+        }
+
         const target = wizardStore.data.target
         if (!target) {
           return

+ 2 - 2
src/components/ui/Dropdowns/ActionDropdown/ActionDropdown.spec.tsx

@@ -15,9 +15,9 @@ 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 ActionDropdown, { Action } from './ActionDropdown'
+import ActionDropdown, { DropdownAction } from './ActionDropdown'
 
-const ACTIONS: Action[] = [
+const ACTIONS: DropdownAction[] = [
   {
     label: 'Action 1',
     title: 'Action 1 Description',

+ 4 - 4
src/components/ui/Dropdowns/ActionDropdown/ActionDropdown.tsx

@@ -53,7 +53,7 @@ const ListStyle = css`
   border: none;
 `
 export const TEST_ID = 'actionDropdown'
-export type Action = {
+export type DropdownAction = {
   label: string,
   color?: string,
   action: () => void,
@@ -64,7 +64,7 @@ export type Action = {
 }
 export type Props = {
   label: string,
-  actions: Action[],
+  actions: DropdownAction[],
   style?: any,
   largeItems?: boolean
 }
@@ -143,14 +143,14 @@ class ActionDropdown extends React.Component<Props, State> {
     this.setState(prevState => ({ showDropdownList: !prevState.showDropdownList }))
   }
 
-  handleItemMouseHover(action: Action, index: number, isEnter: boolean) {
+  handleItemMouseHover(action: DropdownAction, index: number, isEnter: boolean) {
     if (!this.tipRef || index !== 0 || action.disabled) {
       return
     }
     this.tipRef.style.background = isEnter ? ThemePalette.grayscale[0] : ThemePalette.grayscale[1]
   }
 
-  handleItemClick(action: Action) {
+  handleItemClick(action: DropdownAction) {
     if (action.disabled) {
       return
     }

+ 6 - 2
src/components/ui/Lists/FilterList/FilterList.tsx

@@ -18,7 +18,7 @@ import styled from 'styled-components'
 
 import MainListFilter from '@src/components/ui/Lists/MainListFilter'
 import Pagination from '@src/components/ui/Pagination'
-import type { Action as DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
+import type { DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
 import type { ItemComponentProps } from '@src/components/ui/Lists/MainList'
 import MainList from '@src/components/ui/Lists/MainList'
 
@@ -48,9 +48,11 @@ type Props = {
   emptyListMessage?: string,
   emptyListExtraMessage?: string,
   emptyListButtonLabel?: string,
+  emptyListComponent?: React.ReactNode,
   onEmptyListButtonClick?: () => void,
   customFilterComponent?: React.ReactNode,
-  largeDropdownActionItems?: boolean
+  largeDropdownActionItems?: boolean,
+  listHeaderComponent?: React.ReactNode,
 }
 type State = {
   items: any[],
@@ -242,6 +244,7 @@ class FilterList extends React.Component<Props, State> {
           dropdownActions={this.props.dropdownActions || []}
           largeDropdownActionItems={this.props.largeDropdownActionItems}
         />
+        {this.props.listHeaderComponent || null}
         <MainList
           mainListWrapperRef={this.mainListWrapperRef}
           loading={this.props.loading}
@@ -255,6 +258,7 @@ class FilterList extends React.Component<Props, State> {
           emptyListMessage={this.props.emptyListMessage}
           emptyListExtraMessage={this.props.emptyListExtraMessage}
           emptyListButtonLabel={this.props.emptyListButtonLabel}
+          emptyListComponent={this.props.emptyListComponent}
           onEmptyListButtonClick={this.props.onEmptyListButtonClick}
         />
         {this.renderPagination()}

+ 5 - 0
src/components/ui/Lists/MainList/MainList.tsx

@@ -99,6 +99,7 @@ type Props = {
   emptyListButtonLabel?: string,
   onEmptyListButtonClick?: () => void,
   mainListWrapperRef?: React.RefObject<HTMLDivElement>,
+  emptyListComponent?: React.ReactNode,
 }
 @observer
 class MainList extends React.Component<Props> {
@@ -139,6 +140,10 @@ class MainList extends React.Component<Props> {
   }
 
   renderEmptyList() {
+    if (this.props.emptyListComponent) {
+      return this.props.emptyListComponent
+    }
+
     const renderImage = () => {
       if (this.props.emptyListImage) {
         return <EmptyListImage source={this.props.emptyListImage} />

+ 2 - 2
src/components/ui/Lists/MainListFilter/MainListFilter.spec.tsx

@@ -16,7 +16,7 @@ import React, { useState } from 'react'
 import { render } from '@testing-library/react'
 import userEvent from '@testing-library/user-event'
 import MainListFilter from '@src/components/ui/Lists/MainListFilter'
-import { Action } from '@src/components/ui/Dropdowns/ActionDropdown'
+import { DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
 import TestUtils from '@tests/TestUtils'
 import { ThemePalette } from '@src/components/Theme'
 
@@ -27,7 +27,7 @@ const FILTER_ITEMS = [
   { label: 'Items 3', value: 'item-3' },
 ]
 
-const ACTIONS: Action[] = [
+const ACTIONS: DropdownAction[] = [
   {
     label: 'Action 1',
     title: 'Action 1 Description',

+ 1 - 1
src/components/ui/Lists/MainListFilter/MainListFilter.tsx

@@ -23,7 +23,7 @@ import ReloadButton from '@src/components/ui/ReloadButton'
 
 import { ThemePalette, ThemeProps } from '@src/components/Theme'
 
-import type { Action as DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
+import type { DropdownAction } from '@src/components/ui/Dropdowns/ActionDropdown'
 
 const Wrapper = styled.div<any>`
   display: flex;

+ 1 - 0
src/constants.ts

@@ -23,6 +23,7 @@ export const navigationMenu: NavigationMenuType[] = [
   { label: 'Migrations', value: 'migrations' },
   { label: 'Cloud Endpoints', value: 'endpoints' },
   { label: 'Minion Pools', value: 'minion-pools' },
+  { label: 'Bare Metal Servers', value: 'bare-metal-servers' },
 
   // Optional pages
   { label: 'Planning', value: 'planning' },

+ 79 - 57
src/plugins/default/ContentPlugin.tsx

@@ -65,6 +65,78 @@ type Props = {
   handleValidateClick: () => void
   handleCancelClick: () => void
 }
+
+export const findInvalidFields = (schema: Field[], getFieldValue: (field: Field | null) => any) => {
+  const invalidFields = schema.filter(field => {
+    if (field.required) {
+      const value = getFieldValue(field)
+      return !value || value.length === 0
+    }
+    return false
+  }).map(f => f.name)
+
+  return invalidFields
+}
+
+export const renderFields = (opts: {
+  schema: Field[],
+  disabled: boolean,
+  invalidFields: string[],
+  getFieldValue: (field: Field | null) => any,
+  handleFieldChange: (field: Field | null, value: any) => void,
+}) => {
+  const {
+    schema, disabled, invalidFields, getFieldValue, handleFieldChange,
+  } = opts
+  const rows: JSX.Element[] = []
+  let lastField: JSX.Element
+  let i = 0
+  schema.forEach((field, schemaIndex) => {
+    const isPassword = Boolean(configLoader.config.passwordFields.find(fn => field.name === fn))
+        || field.name.indexOf('password') > -1
+    const currentField = (
+      <FieldStyled
+        {...field}
+        label={field.title || LabelDictionary.get(field.name)}
+        width={ThemeProps.inputSizes.large.width}
+        disabled={disabled}
+        password={isPassword}
+        highlight={invalidFields.findIndex(fn => fn === field.name) > -1}
+        value={getFieldValue(field)}
+        onChange={value => { handleFieldChange(field, value) }}
+      />
+    )
+    const pushRow = (field1: React.ReactNode, field2?: React.ReactNode) => {
+      rows.push((
+        <Row key={field.name}>
+          {field1}
+          {field2}
+        </Row>
+      ))
+    }
+    if (field.useTextArea) {
+      pushRow(currentField)
+      i -= 1
+    } else if (i % 2 !== 0) {
+      pushRow(lastField, currentField)
+    } else if (schemaIndex === schema.length - 1) {
+      pushRow(currentField)
+      if (field.useTextArea) {
+        i -= 1
+      }
+    } else {
+      lastField = currentField
+    }
+    i += 1
+  })
+
+  return (
+    <Fields>
+      {rows}
+    </Fields>
+  )
+}
+
 class ContentPlugin extends React.Component<Props> {
   componentDidMount() {
     this.props.onRef(this)
@@ -75,66 +147,16 @@ class ContentPlugin extends React.Component<Props> {
   }
 
   // eslint-disable-next-line react/no-unused-class-component-methods
-  findInvalidFields = () => {
-    const invalidFields = this.props.connectionInfoSchema.filter(field => {
-      if (field.required) {
-        const value = this.props.getFieldValue(field)
-        return !value || value.length === 0
-      }
-      return false
-    }).map(f => f.name)
-
-    return invalidFields
-  }
+  findInvalidFields = () => findInvalidFields(this.props.connectionInfoSchema, this.props.getFieldValue)
 
   renderFields() {
-    const rows: JSX.Element[] = []
-    let lastField: JSX.Element
-    let i = 0
-    this.props.connectionInfoSchema.forEach((field, schemaIndex) => {
-      const isPassword = Boolean(configLoader.config.passwordFields.find(fn => field.name === fn))
-        || field.name.indexOf('password') > -1
-      const currentField = (
-        <FieldStyled
-          {...field}
-          label={field.title || LabelDictionary.get(field.name)}
-          width={ThemeProps.inputSizes.large.width}
-          disabled={this.props.disabled}
-          password={isPassword}
-          highlight={this.props.invalidFields.findIndex(fn => fn === field.name) > -1}
-          value={this.props.getFieldValue(field)}
-          onChange={value => { this.props.handleFieldChange(field, value) }}
-        />
-      )
-      const pushRow = (field1: React.ReactNode, field2?: React.ReactNode) => {
-        rows.push((
-          <Row key={field.name}>
-            {field1}
-            {field2}
-          </Row>
-        ))
-      }
-      if (field.useTextArea) {
-        pushRow(currentField)
-        i -= 1
-      } else if (i % 2 !== 0) {
-        pushRow(lastField, currentField)
-      } else if (schemaIndex === this.props.connectionInfoSchema.length - 1) {
-        pushRow(currentField)
-        if (field.useTextArea) {
-          i -= 1
-        }
-      } else {
-        lastField = currentField
-      }
-      i += 1
+    return renderFields({
+      schema: this.props.connectionInfoSchema,
+      disabled: this.props.disabled,
+      invalidFields: this.props.invalidFields,
+      getFieldValue: this.props.getFieldValue,
+      handleFieldChange: this.props.handleFieldChange,
     })
-
-    return (
-      <Fields>
-        {rows}
-      </Fields>
-    )
   }
 
   render() {

+ 2 - 0
src/plugins/index.ts

@@ -22,6 +22,7 @@ import KubevirtConnectionSchemaPlugin from './kubevirt/ConnectionSchemaPlugin'
 import DefaultContentPlugin from './default/ContentPlugin'
 import AzureContentPlugin from './azure/ContentPlugin'
 import OpenstackContentPlugin from './openstack/ContentPlugin'
+import MetalContentPlugin from './metal/ContentPlugin'
 
 import DefaultOptionsSchemaPlugin from './default/OptionsSchemaPlugin'
 import AwsOptionsSchemaPlugin from './aws/OptionsSchemaPlugin'
@@ -79,6 +80,7 @@ export const ContentPlugin = {
       default: DefaultContentPlugin,
       azure: AzureContentPlugin,
       openstack: OpenstackContentPlugin,
+      metal: MetalContentPlugin,
     }
     if (hasKey(map, provider)) {
       return map[provider]

+ 96 - 0
src/plugins/metal/ContentPlugin.tsx

@@ -0,0 +1,96 @@
+/*
+Copyright (C) 2022  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 styled from 'styled-components'
+
+import type { Field } from '@src/@types/Field'
+
+import { Endpoint, Validation } from '@src/@types/Endpoint'
+import { ThemePalette } from '@src/components/Theme'
+import { Link } from 'react-router-dom'
+import {
+  Wrapper, renderFields, findInvalidFields,
+} from '../default/ContentPlugin'
+
+const ServersInfo = styled.div`
+  font-size: 12px;
+  padding: 8px 32px 0;
+`
+const LinkStyled = styled(Link)`
+  color: ${ThemePalette.primary};
+  text-decoration: none;
+`
+const LinkDiv = styled.span`
+  color: ${ThemePalette.primary};
+  cursor: pointer;
+`
+
+type Props = {
+  connectionInfoSchema: Field[],
+  validation: Validation | null,
+  invalidFields: string[],
+  getFieldValue: (field: Field | null) => any,
+  handleFieldChange: (field: Field | null, value: any) => void,
+  disabled: boolean,
+  cancelButtonText: string,
+  validating: boolean,
+  onRef: (contentPlugin: any) => void,
+  handleFieldsChange: (items: { field: Field, value: any }[]) => void,
+  originalConnectionInfo: Endpoint['connection_info'],
+  onResizeUpdate: (scrollOffset: number) => void,
+  scrollableRef: (ref: HTMLElement) => void,
+  highlightRequired: () => void
+  handleValidateClick: () => void
+  handleCancelClick: () => void
+}
+class ContentPlugin extends React.Component<Props> {
+  componentDidMount() {
+    this.props.onRef(this)
+  }
+
+  componentWillUnmount() {
+    this.props.onRef(undefined)
+  }
+
+  // eslint-disable-next-line react/no-unused-class-component-methods
+  findInvalidFields = () => findInvalidFields(this.props.connectionInfoSchema, this.props.getFieldValue)
+
+  renderFields() {
+    return renderFields({
+      schema: this.props.connectionInfoSchema,
+      getFieldValue: this.props.getFieldValue,
+      handleFieldChange: this.props.handleFieldChange,
+      disabled: this.props.disabled,
+      invalidFields: this.props.invalidFields,
+    })
+  }
+
+  render() {
+    const link = window.location.pathname === '/bare-metal-servers'
+      ? (
+        <LinkDiv onClick={() => window.location.reload()}>hub servers page</LinkDiv>
+      ) : (
+        <LinkStyled to="/bare-metal-servers">hub servers page</LinkStyled>
+      )
+    return (
+      <Wrapper>
+        <ServersInfo>To add a server to an existing hub, use the {link}.</ServersInfo>
+        {this.renderFields()}
+      </Wrapper>
+    )
+  }
+}
+
+export default ContentPlugin

+ 2 - 2
src/sources/AzureSource.ts

@@ -30,7 +30,7 @@ const assessedVmsUrl = ({ ...other }) => `${assessmentDetailsUrl({ ...other })}/
 
 class Util {
   static buildUrl(baseUrl: string, apiVersion?: string): string {
-    const url = `/proxy/${DomUtils.encodeToBase64Url(`${azureUrl + baseUrl}?api-version=${apiVersion || defaultApiVersion}`)}`
+    const url = `/proxy/azure/${DomUtils.encodeToBase64Url(`${azureUrl + baseUrl}?api-version=${apiVersion || defaultApiVersion}`)}`
     return url
   }
 
@@ -76,7 +76,7 @@ class Util {
 class AzureSource {
   static authenticate(connectionInfo: any): Promise<any> {
     return Api.send({
-      url: '/azure-login',
+      url: '/proxy/azure/login',
       method: 'POST',
       data: connectionInfo,
     }).then(response => {

+ 4 - 0
src/sources/InstanceSource.ts

@@ -22,6 +22,10 @@ import { ProviderTypes } from '@src/@types/Providers'
 import DomUtils from '@src/utils/DomUtils'
 
 class InstanceSource {
+  removeInstancesFromCache(endpointId: string) {
+    Api.removeFromCache(`${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/endpoints/${endpointId}/instances`)
+  }
+
   async loadInstancesChunk(opts: {
     endpointId: string,
     chunkSize: number,

+ 94 - 0
src/sources/MetalHubSource.ts

@@ -0,0 +1,94 @@
+/*
+Copyright (C) 202  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 { Endpoint } from '@src/@types/Endpoint'
+import { MetalHubServer } from '@src/@types/MetalHub'
+import EndpointSource from '@src/sources/EndpointSource'
+import apiCaller from '@src/utils/ApiCaller'
+import configLoader from '@src/utils/Config'
+
+class MetalHubSource {
+  private _endpoint: Endpoint | null = null
+
+  async getMetalHubEndpoint(): Promise<Endpoint> {
+    if (this._endpoint) {
+      return this._endpoint
+    }
+
+    const endpoints = await EndpointSource.getEndpoints(true)
+    const metalHubEndpointName = configLoader.config.bareMetalEndpointName
+    const metalHubEndpoint = endpoints.find(endpoint => endpoint.name === metalHubEndpointName)
+    if (!metalHubEndpoint) {
+      throw new Error(`Could not find endpoint '${metalHubEndpointName}'. The endpoint name was configured in the config file and is needed in order to communicate with the Coriolis Metal Hub service.`)
+    }
+
+    return metalHubEndpoint
+  }
+
+  async getServers(skipLog?: boolean): Promise<MetalHubServer[]> {
+    const response = await apiCaller.send({
+      url: `${configLoader.config.servicesUrls.metalhub}/servers`,
+      skipLog,
+      quietError: true,
+    })
+    return response.data
+  }
+
+  async getServerDetails(serverId: number): Promise<MetalHubServer> {
+    const response = await apiCaller.send({
+      url: `${configLoader.config.servicesUrls.metalhub}/servers/${serverId}`,
+    })
+    return response.data
+  }
+
+  async loadFingerprint(): Promise<string> {
+    return (await apiCaller.send({ url: '/proxy/metal-hub/fingerprint', quietError: true })).data
+  }
+
+  async addServer(apiEndpoint: string): Promise<MetalHubServer> {
+    const response = await apiCaller.send({
+      url: `${configLoader.config.servicesUrls.metalhub}/servers`,
+      method: 'POST',
+      data: { api_endpoint: apiEndpoint },
+    })
+    return response.data
+  }
+
+  async deleteServer(serverId: number): Promise<void> {
+    const response = await apiCaller.send({
+      url: `${configLoader.config.servicesUrls.metalhub}/servers/${serverId}`,
+      method: 'DELETE',
+    })
+    return response.data
+  }
+
+  async patchServer(serverId: number, apiEndpoint: string): Promise<MetalHubServer> {
+    const response = await apiCaller.send({
+      url: `${configLoader.config.servicesUrls.metalhub}/servers/${serverId}`,
+      method: 'PUT',
+      data: { api_endpoint: apiEndpoint },
+    })
+    return response.data
+  }
+
+  async refreshServer(serverId: number): Promise<MetalHubServer> {
+    const response = await apiCaller.send({
+      url: `${configLoader.config.servicesUrls.metalhub}/servers/${serverId}/refresh`,
+      method: 'GET',
+    })
+    return response.data
+  }
+}
+
+export default new MetalHubSource()

+ 2 - 2
src/stores/InstanceStore.ts

@@ -119,10 +119,10 @@ class InstanceStore {
         instances, instancesCount: instances.length + invalidInstances.length, chunkCount, reload,
       })
       if (shouldContinue) {
-        loadNextChunk(instances[instances.length - 1].id)
+        await loadNextChunk(instances[instances.length - 1].id)
       }
     }
-    loadNextChunk()
+    await loadNextChunk()
   }
 
   @action loadInstancesInChunksSuccess(opts: {

+ 134 - 0
src/stores/MetalHubStore.ts

@@ -0,0 +1,134 @@
+/*
+Copyright (C) 2022  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 { observable, action, runInAction } from 'mobx'
+
+import source from '@src/sources/MetalHubSource'
+import { MetalHubServer } from '@src/@types/MetalHub'
+
+class MetalHubStore {
+  @observable servers: MetalHubServer[] = []
+
+  @observable loadingServers: boolean = false
+
+  @observable loadingServersError: string = ''
+
+  @observable fingerprint: string = ''
+
+  @observable loadingFingerprint: boolean = false
+
+  @observable loadingFingerprintError: string = ''
+
+  @observable loadingNewServer: boolean = false
+
+  @observable serverDetails: MetalHubServer | null = null
+
+  @observable loadingServerDetails: boolean = false
+
+  @observable updatingServer: boolean = false
+
+  @observable refreshingServer: boolean = false
+
+  async getMetalHubEndpoint() {
+    return source.getMetalHubEndpoint()
+  }
+
+  @action async getServers(options?: { showLoading?: boolean, skipLog?: boolean }) {
+    if (options?.showLoading) {
+      this.loadingServers = true
+    }
+    try {
+      const servers = await source.getServers(options?.skipLog)
+      runInAction(() => {
+        this.servers = servers
+        this.loadingServersError = ''
+      })
+    } finally {
+      runInAction(() => { this.loadingServers = false })
+    }
+  }
+
+  @action async loadFingerprint() {
+    this.loadingFingerprint = true
+    try {
+      const fingerprint = await source.loadFingerprint()
+      runInAction(() => {
+        this.fingerprint = fingerprint
+        this.loadingFingerprintError = ''
+      })
+    } catch (err) {
+      runInAction(() => {
+        this.loadingFingerprintError = err.data?.error?.message || err.message || ''
+      })
+    } finally {
+      runInAction(() => { this.loadingFingerprint = false })
+    }
+  }
+
+  @action async addServer(endpoint: string) {
+    this.loadingNewServer = true
+    try {
+      const addedServer = await source.addServer(endpoint)
+      runInAction(() => {
+        this.servers.push(addedServer)
+      })
+    } finally {
+      runInAction(() => { this.loadingNewServer = false })
+    }
+  }
+
+  @action async getServerDetails(serverId: number) {
+    this.loadingServerDetails = true
+
+    try {
+      const server = await source.getServerDetails(serverId)
+      runInAction(() => {
+        this.serverDetails = server
+      })
+    } finally {
+      runInAction(() => { this.loadingServerDetails = false })
+    }
+  }
+
+  @action async deleteServer(serverId: number) {
+    await source.deleteServer(serverId)
+  }
+
+  clearServerDetails() {
+    this.serverDetails = null
+  }
+
+  @action async patchServer(serverId: number, apiEndpoint: string) {
+    this.updatingServer = true
+    try {
+      await source.patchServer(serverId, apiEndpoint)
+    } finally {
+      runInAction(() => { this.updatingServer = false })
+    }
+  }
+
+  @action async refreshServer(serverId: number) {
+    this.refreshingServer = true
+    try {
+      const server = await source.refreshServer(serverId)
+      runInAction(() => {
+        this.serverDetails = server
+      })
+    } finally {
+      runInAction(() => { this.refreshingServer = false })
+    }
+  }
+}
+
+export default new MetalHubStore()

+ 4 - 0
src/stores/ProviderStore.ts

@@ -288,6 +288,10 @@ class ProviderStore {
 
   getOptionsValuesLastDirection: 'source' | 'destination' | '' = ''
 
+  hasExecuteNowOptions(provider: ProviderTypes) {
+    return configLoader.config.providersDisabledExecuteOptions.indexOf(provider) === -1
+  }
+
   async getOptionsValues(config: {
     optionsType: 'source' | 'destination',
     endpointId: string,

+ 5 - 2
src/stores/WizardStore.ts

@@ -350,9 +350,12 @@ class WizardStore {
       return
     }
     this.data = state.data
-    this.schedules = state.schedules
-    this.storageMap = state.storageMap
+    this.schedules = state.schedules || []
+    this.storageMap = state.storageMap || []
     this.defaultStorage = state.defaultStorage
+    if (state.currentPage) {
+      this.setCurrentPage(state.currentPage)
+    }
   }
 
   @action cancelUploadedScript(global: string | null, instanceName: string | null) {

+ 4 - 0
src/utils/ApiCaller.ts

@@ -57,6 +57,10 @@ class ApiCaller {
     return cookie.get('projectId') || 'undefined'
   }
 
+  removeFromCache(url: string) {
+    cacher.remove(url)
+  }
+
   cancelRequests(cancelRequestId: string) {
     const filteredCancelables = cancelables.filter(r => r.requestId === cancelRequestId)
     filteredCancelables.forEach(c => {

+ 1 - 1
src/utils/ApiCallerHandlers.ts

@@ -52,7 +52,7 @@ class ApiCallerHandlers {
       })
     }
 
-    if (error.request.responseURL.indexOf('/proxy/') === -1 && error.request.responseURL.indexOf('/azure-login') === -1) {
+    if (error.request.responseURL.indexOf('/proxy/azure/') === -1 && error.request.responseURL.indexOf('/proxy/azure/login') === -1) {
       redirect(error.response.status)
     }
 

+ 10 - 0
src/utils/Cacher.ts

@@ -47,6 +47,16 @@ class Cacher {
 
     localStorage.setItem(STORE, JSON.stringify(storage))
   }
+
+  remove(keyStartsWith: string) {
+    const storage: Cache = JSON.parse(localStorage.getItem(STORE) || '{}')
+    Object.keys(storage).forEach(key => {
+      if (key.startsWith(keyStartsWith)) {
+        delete storage[key]
+      }
+    })
+    localStorage.setItem(STORE, JSON.stringify(storage))
+  }
 }
 
 export default new Cacher()

+ 4 - 1
webpack.dev.js

@@ -8,7 +8,10 @@ module.exports = merge(common, {
     port: 3001,
     hot: true,
     historyApiFallback: true,
-    proxy: { '/api': `http://localhost:${process.env.PORT || 3000}` },
+    proxy: {
+      '/api': `http://localhost:${process.env.PORT || 3000}`,
+      '/proxy': `http://localhost:${process.env.PORT || 3000}`,
+    },
     stats: 'minimal',
   },
 })