Просмотр исходного кода

Display SAP licence allowances separately

The dashboard renders one set of `replica/migration` charts per licence
edition the appliance holds licences for, headed by the edition name once
there is more than one. An appliance with `no SAP` licence renders as
before.

The licence module shows the editions in use, and both it and the setup
store build the appliance ID off the standard licence version.

Signed-off-by: Mihaela Balutoiu <mbalutoiu@cloudbasesolutions.com>
Mihaela Balutoiu 3 недель назад
Родитель
Сommit
84d6d707ec

+ 103 - 6
src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.spec.tsx

@@ -15,17 +15,19 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import { DateTime } from "luxon";
 import React from "react";
 
-import { Licence, LicenceServerStatus } from "@src/@types/Licence";
+import {
+  Licence,
+  LicenceServerStatus,
+  LicenceStats,
+} from "@src/@types/Licence";
+import LicenceUtils from "@src/utils/LicenceUtils";
 import { render } from "@testing-library/react";
 import TestUtils from "@tests/TestUtils";
 
 import DashboardLicence from "./DashboardLicence";
 
 describe("DashboardLicence", () => {
-  const futureLicence: Licence = {
-    applianceId: "test-id",
-    earliestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(),
-    latestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(),
+  const standardStats: LicenceStats = {
     currentPerformedReplicas: 5,
     currentPerformedMigrations: 3,
     lifetimePerformedMigrations: 4,
@@ -36,6 +38,25 @@ describe("DashboardLicence", () => {
     lifetimeAvailableMigrations: 10,
   };
 
+  const sapStats: LicenceStats = {
+    currentPerformedReplicas: 2,
+    currentPerformedMigrations: 1,
+    lifetimePerformedMigrations: 1,
+    lifetimePerformedReplicas: 2,
+    currentAvailableReplicas: 4,
+    currentAvailableMigrations: 8,
+    lifetimeAvailableReplicas: 4,
+    lifetimeAvailableMigrations: 8,
+  };
+
+  const futureLicence: Licence = {
+    applianceId: "test-id",
+    earliestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(),
+    latestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(),
+    standardStats,
+    sapStats: LicenceUtils.emptyStats(),
+  };
+
   const status: LicenceServerStatus = {
     hostname: "test-hostname",
     multi_appliance: false,
@@ -170,7 +191,7 @@ describe("DashboardLicence", () => {
       ...defaultProps,
       licence: {
         ...futureLicence,
-        currentPerformedReplicas: 1,
+        standardStats: { ...standardStats, currentPerformedReplicas: 1 },
       },
     };
 
@@ -180,4 +201,80 @@ describe("DashboardLicence", () => {
         .textContent,
     ).toBe("1 Used Replica ");
   });
+
+  it("renders only the standard charts, unlabelled, without an SAP licence", () => {
+    render(<DashboardLicence {...defaultProps} />);
+
+    expect(
+      TestUtils.selectAll("DashboardLicence__ChartHeaderCurrent-").length,
+    ).toBe(2);
+    expect(TestUtils.selectAll("DashboardLicence__KindLabel-").length).toBe(0);
+  });
+
+  it("renders labelled standard and SAP charts when both licences are held", () => {
+    const newProps = {
+      ...defaultProps,
+      licence: { ...futureLicence, sapStats },
+    };
+    render(<DashboardLicence {...newProps} />);
+
+    expect(
+      Array.from(TestUtils.selectAll("DashboardLicence__KindLabel-")).map(
+        el => el.textContent,
+      ),
+    ).toEqual(["Standard", "SAP"]);
+
+    const currents = Array.from(
+      TestUtils.selectAll("DashboardLicence__ChartHeaderCurrent-"),
+    ).map(el => el.textContent);
+    const totals = Array.from(
+      TestUtils.selectAll("DashboardLicence__ChartHeaderTotal-"),
+    ).map(el => el.textContent);
+
+    expect(currents).toEqual([
+      "5 Used Replicas ",
+      "3 Used Migrations ",
+      "2 Used Replicas ",
+      "1 Used Migration ",
+    ]);
+    expect(totals).toEqual(["Total 10", "Total 5", "Total 4", "Total 8"]);
+  });
+
+  it("renders only the SAP charts for an SAP-only appliance", () => {
+    const newProps = {
+      ...defaultProps,
+      licence: {
+        ...futureLicence,
+        standardStats: LicenceUtils.emptyStats(),
+        sapStats,
+      },
+    };
+    render(<DashboardLicence {...newProps} />);
+
+    expect(TestUtils.selectAll("DashboardLicence__KindLabel-").length).toBe(0);
+    expect(
+      Array.from(
+        TestUtils.selectAll("DashboardLicence__ChartHeaderTotal-"),
+      ).map(el => el.textContent),
+    ).toEqual(["Total 4", "Total 8"]);
+  });
+
+  it("does not pick the SAP version for the appliance ID of an expired licence", () => {
+    const newProps = {
+      ...defaultProps,
+      licence: {
+        ...futureLicence,
+        earliestLicenceExpiryDate: DateTime.now().minus({ days: 2 }).toJSDate(),
+      },
+      licenceServerStatus: {
+        ...status,
+        supported_licence_versions: ["v2-sap", "v2", "v1"],
+      },
+    };
+    render(<DashboardLicence {...newProps} />);
+
+    expect(
+      TestUtils.select("DashboardLicence__ApplianceId-")?.textContent,
+    ).toBe("Appliance ID:test-id-licencev2");
+  });
 });

+ 68 - 31
src/components/modules/DashboardModule/DashboardLicence/DashboardLicence.tsx

@@ -23,9 +23,14 @@ import CopyMultineValue from "@src/components/ui/CopyMultilineValue";
 import InfoIcon from "@src/components/ui/InfoIcon";
 import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
 import DateUtils from "@src/utils/DateUtils";
+import LicenceUtils from "@src/utils/LicenceUtils";
 import ObjectUtils from "@src/utils/ObjectUtils";
 
-import type { Licence, LicenceServerStatus } from "@src/@types/Licence";
+import type {
+  Licence,
+  LicenceKind,
+  LicenceServerStatus,
+} from "@src/@types/Licence";
 const Wrapper = styled.div<any>`
   flex-grow: 1;
 `;
@@ -40,7 +45,7 @@ const Module = styled.div<any>`
   overflow: auto;
   border-radius: ${ThemeProps.borderRadius};
   padding: 24px 16px 16px 16px;
-  height: 232px;
+  min-height: 232px;
 `;
 const LicenceInfo = styled.div<any>`
   width: 100%;
@@ -107,6 +112,18 @@ const TopInfoDateBottom = styled.div<any>`
 const Charts = styled.div<any>`
   margin-top: -8px;
 `;
+const KindGroup = styled.div`
+  & + & {
+    margin-top: 8px;
+  }
+`;
+const KindLabel = styled.div`
+  font-size: 10px;
+  font-weight: ${ThemeProps.fontWeights.medium};
+  text-transform: uppercase;
+  color: ${ThemePalette.grayscale[4]};
+  margin-top: 24px;
+`;
 const ChartRow = styled.div`
   display: flex;
   margin-left: -32px;
@@ -194,29 +211,60 @@ class DashboardLicence extends React.Component<Props> {
     }
   }
 
-  renderLicenceStatusText(info: Licence): React.ReactNode {
+  renderKindCharts(info: Licence, kind: LicenceKind) {
+    const stats = LicenceUtils.getStats(info, kind);
+    const kindLabel = LicenceUtils.getKindLabel(kind);
     const graphDataRows = [
       [
         {
           color: ThemePalette.alert,
-          current: info.currentPerformedReplicas,
-          total: info.currentAvailableReplicas,
+          current: stats.currentPerformedReplicas,
+          total: stats.currentAvailableReplicas,
           label: "Used Replica",
           info: `The number of replicas fulfilled over the number of replicas available in
-          all currently active licences (including non-activated floating licences)`,
+          all currently active ${kindLabel} licences (including non-activated floating licences)`,
         },
       ],
       [
         {
           color: ThemePalette.primary,
-          current: info.currentPerformedMigrations,
-          total: info.currentAvailableMigrations,
+          current: stats.currentPerformedMigrations,
+          total: stats.currentAvailableMigrations,
           label: "Used Migration",
           info: `The number of migrations fulfilled over the number of migrations available in
-          all currently active licences (including non-activated floating licences)`,
+          all currently active ${kindLabel} licences (including non-activated floating licences)`,
         },
       ],
     ];
+
+    return graphDataRows.map((row, i) => (
+      <ChartRow key={`${kind}-${i}`}>
+        {row.map(data => (
+          <Chart key={data.label}>
+            <ChartHeader>
+              <ChartHeaderCurrent>
+                {data.current}{" "}
+                {data.current === 1 ? data.label : `${data.label}s`}{" "}
+                <InfoIcon marginBottom={-3} text={data.info} />
+              </ChartHeaderCurrent>
+              <ChartHeaderTotal>Total {data.total}</ChartHeaderTotal>
+            </ChartHeader>
+            <ChartBodyWrapper>
+              <ChartBody
+                color={data.color}
+                width={data.total ? (data.current / data.total) * 100 : 0}
+              />
+            </ChartBodyWrapper>
+          </Chart>
+        ))}
+      </ChartRow>
+    ));
+  }
+
+  renderLicenceStatusText(info: Licence): React.ReactNode {
+    const kinds = LicenceUtils.getActiveKinds(info);
+    // the flavour headings only earn their space once there's more than one:
+    const showKindLabels = kinds.length > 1;
     const expirationData = DateUtils.getLocalDate(
       info.earliestLicenceExpiryDate,
     );
@@ -235,27 +283,13 @@ class DashboardLicence extends React.Component<Props> {
           </TopInfoDate>
         </TopInfo>
         <Charts>
-          {graphDataRows.map((row, i) => (
-            <ChartRow key={i}>
-              {row.map(data => (
-                <Chart key={data.label}>
-                  <ChartHeader>
-                    <ChartHeaderCurrent>
-                      {data.current}{" "}
-                      {data.current === 1 ? data.label : `${data.label}s`}{" "}
-                      <InfoIcon marginBottom={-3} text={data.info} />
-                    </ChartHeaderCurrent>
-                    <ChartHeaderTotal>Total {data.total}</ChartHeaderTotal>
-                  </ChartHeader>
-                  <ChartBodyWrapper>
-                    <ChartBody
-                      color={data.color}
-                      width={(data.current / data.total) * 100}
-                    />
-                  </ChartBodyWrapper>
-                </Chart>
-              ))}
-            </ChartRow>
+          {kinds.map(kind => (
+            <KindGroup key={kind}>
+              {showKindLabels ? (
+                <KindLabel>{LicenceUtils.getKindLabel(kind)}</KindLabel>
+              ) : null}
+              {this.renderKindCharts(info, kind)}
+            </KindGroup>
           ))}
         </Charts>
       </LicenceInfo>
@@ -273,7 +307,10 @@ class DashboardLicence extends React.Component<Props> {
   }
 
   renderLicenceExpired(licence: Licence, serverStatus: LicenceServerStatus) {
-    const applianceId = `${licence.applianceId}-licence${serverStatus.supported_licence_versions[0]}`;
+    const applianceId = LicenceUtils.getApplianceIdWithVersion(
+      licence.applianceId,
+      serverStatus,
+    );
     return (
       <LicenceError>
         <p>

+ 54 - 5
src/components/modules/LicenceModule/LicenceModule.spec.tsx

@@ -15,7 +15,12 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import { DateTime } from "luxon";
 import React from "react";
 
-import { Licence, LicenceServerStatus } from "@src/@types/Licence";
+import {
+  Licence,
+  LicenceServerStatus,
+  LicenceStats,
+} from "@src/@types/Licence";
+import LicenceUtils from "@src/utils/LicenceUtils";
 import { fireEvent, render, waitFor } from "@testing-library/react";
 import TestUtils from "@tests/TestUtils";
 
@@ -30,10 +35,7 @@ jest.mock("@src/components/ui/StatusComponents/StatusImage", () => ({
   ),
 }));
 
-const FUTURE_LICENCE: Licence = {
-  applianceId: "test-id",
-  earliestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(),
-  latestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(),
+const STANDARD_STATS: LicenceStats = {
   currentPerformedReplicas: 5,
   currentPerformedMigrations: 3,
   lifetimePerformedMigrations: 4,
@@ -44,6 +46,25 @@ const FUTURE_LICENCE: Licence = {
   lifetimeAvailableMigrations: 10,
 };
 
+const SAP_STATS: LicenceStats = {
+  currentPerformedReplicas: 2,
+  currentPerformedMigrations: 1,
+  lifetimePerformedMigrations: 1,
+  lifetimePerformedReplicas: 2,
+  currentAvailableReplicas: 4,
+  currentAvailableMigrations: 8,
+  lifetimeAvailableReplicas: 4,
+  lifetimeAvailableMigrations: 8,
+};
+
+const FUTURE_LICENCE: Licence = {
+  applianceId: "test-id",
+  earliestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(),
+  latestLicenceExpiryDate: DateTime.now().plus({ years: 1 }).toJSDate(),
+  standardStats: STANDARD_STATS,
+  sapStats: LicenceUtils.emptyStats(),
+};
+
 const SERVER_STATUS: LicenceServerStatus = {
   hostname: "test-hostname",
   multi_appliance: false,
@@ -74,6 +95,34 @@ describe("LicenceModule", () => {
     getByText("test-id-licencev2");
   });
 
+  it("renders the Standard edition for an appliance with no SAP licence", () => {
+    const { getByText } = render(<LicenceModule {...defaultProps} />);
+    getByText("Standard");
+  });
+
+  it("renders both editions for an appliance holding an SAP licence too", () => {
+    const { getByText } = render(
+      <LicenceModule
+        {...defaultProps}
+        licenceInfo={{ ...FUTURE_LICENCE, sapStats: SAP_STATS }}
+      />,
+    );
+    getByText("Standard, SAP");
+  });
+
+  it("keeps the non-SAP licence version in the appliance ID", () => {
+    const { getByText } = render(
+      <LicenceModule
+        {...defaultProps}
+        licenceServerStatus={{
+          ...SERVER_STATUS,
+          supported_licence_versions: ["v2-sap", "v2", "v1"],
+        }}
+      />,
+    );
+    getByText("test-id-licencev2");
+  });
+
   it("changes to add mode when add button is clicked", () => {
     const { getByText } = render(<LicenceModule {...defaultProps} />);
     getByText("Add Licence").click();

+ 15 - 1
src/components/modules/LicenceModule/LicenceModule.tsx

@@ -27,6 +27,7 @@ import TextArea from "@src/components/ui/TextArea";
 import { LEGAL_URLS } from "@src/constants";
 import DateUtils from "@src/utils/DateUtils";
 import FileUtils from "@src/utils/FileUtils";
+import LicenceUtils from "@src/utils/LicenceUtils";
 
 import licenceImage from "./images/licence";
 
@@ -334,12 +335,25 @@ class LicenceModule extends React.Component<Props, State> {
             {this.renderLicenceStatusText(info)}
           </LicenceRowContent>
         </LicenceRow>
+        <LicenceRow>
+          <LicenceRowContent>
+            <LicenceRowLabel>Licence Edition</LicenceRowLabel>
+            <LicenceRowDescription>
+              {LicenceUtils.getActiveKinds(info)
+                .map(kind => LicenceUtils.getKindLabel(kind))
+                .join(", ")}
+            </LicenceRowDescription>
+          </LicenceRowContent>
+        </LicenceRow>
         <LicenceRow>
           <LicenceRowContent>
             <LicenceRowLabel>Appliance ID</LicenceRowLabel>
             <LicenceRowDescription>
               <CopyValue
-                value={`${info.applianceId}-licence${status.supported_licence_versions[0]}`}
+                value={LicenceUtils.getApplianceIdWithVersion(
+                  info.applianceId,
+                  status,
+                )}
               />
             </LicenceRowDescription>
           </LicenceRowContent>

+ 5 - 1
src/stores/SetupStore.ts

@@ -23,6 +23,7 @@ import {
 } from "../@types/InitialSetup";
 import lincenceSource from "../sources/LincenceSource";
 import configLoader from "../utils/Config";
+import LicenceUtils from "../utils/LicenceUtils";
 import ObjectUtils from "../utils/ObjectUtils";
 
 export const customerInfoSetupStoreValueToString = (
@@ -73,7 +74,10 @@ class SetupStore {
         return;
       }
       runInAction(() => {
-        this.applianceId = `${ids[0]}-licence${status.supported_licence_versions[0]}`;
+        this.applianceId = LicenceUtils.getApplianceIdWithVersion(
+          ids[0],
+          status,
+        );
       });
     } catch (err) {
       this.applianceIdError =