Преглед изворни кода

Add Coriolis Bare Metal Servers support

There's a new page for displaying the list of Coriolis Bare Metal
Servers available in Coriolis Bare Metal Hub appliance. The servers can
be added, deleted, replicated and migrated.

The Coriolis Bare Metal Hub Fingerprint is displayed on the Servers list
page.

The name of the Coriolis Bare Metal Hub is configurable and moddable
with bareMetalEndpointName.

CA_FINGERPRINT must be set as an environment variable with the path to
the Fingerprint file in order for it to be shown in the UI.
Sergiu Miclea пре 3 година
родитељ
комит
2c9750fce5
62 измењених фајлова са 2834 додато и 338 уклоњено
  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',
   },
 })