ソースを参照

Adding <Metric /> components to Taipy-gui.
Adding pytest cases for Metric.tsx components

namnguyen 1 年間 前
コミット
06892d0be2

+ 31 - 0
frontend/taipy-gui/src/components/Taipy/Metric.spec.tsx

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+import React from "react";
+import {render} from "@testing-library/react";
+import "@testing-library/jest-dom";
+
+import Metric from "./Metric";
+
+describe("Metric Component", () => {
+  it("renders", async () => {
+    const { getByTestId } = render(<Metric testId={'test-id'}/>);
+    const elt = getByTestId("test-id");
+    expect(elt.tagName).toBe("DIV");
+  })
+  it("displays the right info for class", async () => {
+    const { getByTestId } = render(<Metric testId={'test-id'} className={'taipy-gauge'}/>);
+    const elt = getByTestId('test-id');
+    expect(elt).toHaveClass('taipy-gauge');
+  })
+});

+ 143 - 0
frontend/taipy-gui/src/components/Taipy/Metric.tsx

@@ -0,0 +1,143 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+import React,
+{
+  useMemo,
+  lazy,
+  useState,
+  useEffect,
+} from 'react';
+import {Delta} from "plotly.js";
+import {useClassNames, useDynamicProperty} from "../../utils/hooks";
+import {TaipyBaseProps, TaipyHoverProps} from "./utils";
+import Box from "@mui/material/Box";
+
+const Plot = lazy(() => import("react-plotly.js"));
+
+interface MetricProps extends TaipyBaseProps, TaipyHoverProps {
+  type?: string
+  min?: number
+  max?: number
+  value?: number
+  defaultValue?: number
+  delta?: number
+  defaultDelta?: number
+  thresholdValue?: number
+  defaultThresholdValue?: number
+  format?: string
+  formatDelta?: string
+  testId?: string
+}
+
+interface DeltaProps extends Partial<Delta> {
+  suffix: string
+}
+
+const Metric = (props: MetricProps) => {
+  const metricValue = useDynamicProperty(props.value, props.defaultValue, 0)
+  const gaugeThresholdValue = useDynamicProperty(props.thresholdValue, props.defaultThresholdValue, undefined)
+  const metricDelta = useDynamicProperty(props.delta, props.defaultDelta, undefined)
+  const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
+
+  const [metricType, setMetricType] = useState<"angular" | "bullet">("angular");
+  const [formatType, setFormatType] = useState<"%" | "">("");
+  const [isDeltaFormatPercentage, setIsDeltaFormatPercentage] = useState<"%" | "">("");
+
+  useEffect(() => {
+  switch (props.type) {
+    case "circular":
+      setMetricType("angular");
+      break;
+    case "linear":
+      setMetricType("bullet");
+      break;
+    default:
+      setMetricType("angular");
+  }
+}, [props.type]);
+
+useEffect(() => {
+  switch (props.format) {
+    case "%.2f%%":
+      setFormatType("%");
+      break;
+    default:
+      setFormatType("");
+  }
+}, [props.format]);
+
+useEffect(() => {
+  switch (props.formatDelta) {
+    case "%.2f%%":
+      setIsDeltaFormatPercentage("%");
+      break;
+    default:
+      setIsDeltaFormatPercentage("");
+  }
+}, [props.formatDelta]);
+
+  const refValue = useMemo(() => {
+    if (typeof metricValue === 'number' && typeof metricDelta === 'number') {
+      return metricValue - metricDelta;
+    } else {
+      return;
+    }
+  }, [metricValue, metricDelta]);
+
+  const extendedDelta: DeltaProps = {
+    reference: refValue,
+    suffix: isDeltaFormatPercentage,
+  }
+
+  return (
+    <Box data-testid={props.testId} className={className}>
+        <Plot
+        data={[
+          {
+            domain: {x: [0, 1], y: [0, 1]},
+            value: metricValue,
+            type: "indicator",
+            mode: "gauge+number+delta",
+            number: {
+              suffix: formatType
+            },
+            delta: extendedDelta,
+            gauge: {
+              axis: {range: [props.min, props.max]},
+              shape: metricType,
+              threshold: {
+                line: {color: "red", width: 4},
+                thickness: 0.75,
+                value: gaugeThresholdValue
+              }
+            },
+          }
+        ]}
+        layout={{
+          paper_bgcolor: "#fff",
+          width: 600,
+          height: 600,
+        }}
+        style={{
+          position: "relative",
+          display: "inline-block",
+          borderRadius: "20px",
+          overflow: "hidden",
+        }}
+      />
+    </Box>
+  );
+}
+
+export default Metric;

+ 2 - 0
frontend/taipy-gui/src/components/Taipy/index.ts

@@ -38,6 +38,7 @@ import StatusList from "./StatusList";
 import Table from "./Table";
 import Toggle from "./Toggle";
 import TreeView from "./TreeView";
+import Metric from "./Metric";
 
 const registeredComponents: Record<string, ComponentType> = {};
 
@@ -60,6 +61,7 @@ export const getRegisteredComponents = () => {
             Login: Login,
             Layout: Layout,
             MenuCtl: MenuCtl,
+            Metric: Metric,
             NavBar: NavBar,
             PageContent: PageContent,
             Pane: Pane,

+ 2 - 0
frontend/taipy-gui/src/extensions/exports.ts

@@ -16,6 +16,7 @@ import Dialog from "../components/Taipy/Dialog";
 import Login from "../components/Taipy/Login";
 import Router from "../components/Router";
 import Table from "../components/Taipy/Table";
+import Metric from "../components/Taipy/Metric";
 import { useLovListMemo, LoV, LoVElt } from "../components/Taipy/lovUtils";
 import { LovItem } from "../utils/lov";
 import { getUpdateVar } from "../components/Taipy/utils";
@@ -36,6 +37,7 @@ export {
     Login,
     Router,
     Table,
+    Metric,
     TaipyContext as Context,
     createRequestDataUpdateAction,
     createRequestUpdateAction,

+ 22 - 0
taipy/gui/_renderers/factory.py

@@ -56,6 +56,7 @@ class _Factory:
         "text": "value",
         "toggle": "value",
         "tree": "value",
+        "metric": "value"
     }
 
     _TEXT_ATTRIBUTES = ["format", "id", "hover_text", "raw"]
@@ -331,6 +332,27 @@ class _Factory:
             ]
         )
         ._set_propagate(),
+        "metric": lambda gui, control_type, attrs: _Builder(
+            gui=gui,
+            control_type=control_type,
+            element_name="Metric",
+            attributes=attrs,
+        )
+        .set_value_and_default(var_type=PropertyType.dynamic_number)
+        .set_attributes(
+            [
+                ("id",),
+                ("active", PropertyType.dynamic_boolean, True),
+                ("hover_text", PropertyType.dynamic_string),
+                ("value", PropertyType.dynamic_number),
+                ("type", PropertyType.string, "circular"),
+                ("min", PropertyType.number, 0),
+                ("max", PropertyType.number, 100),
+                ("delta", PropertyType.dynamic_number),
+                ("format",),
+                ("format_delta",),
+            ]
+        ),
         "navbar": lambda gui, control_type, attrs: _Builder(
             gui=gui, control_type=control_type, element_name="NavBar", attributes=attrs, default_value=None
         ).set_attributes(

+ 44 - 0
taipy/gui/viselements.json

@@ -773,6 +773,50 @@
         ]
       }
     ],
+    [
+      "metric",
+      {
+        "inherits": [
+          "shared"
+        ],
+        "properties": [
+          {
+            "name": "value",
+            "default_property": true,
+            "type": "dynamic(int|float)",
+            "doc": "The value to display."
+          },
+          {
+            "name": "type",
+            "default_value": "circular",
+            "type": "str",
+            "doc": "The type of the gauge.<br/>Possible values are:\n<ul>\n<li>\"none\"</li>\n<li>\"circular\"</li>\n<li>\"linear\"</li></ul>"
+          },
+          {
+            "name": "min",
+            "type": "int|float",
+            "default_value": 0,
+            "doc": "The minimum value of the metric indicator"
+          },
+          {
+            "name": "max",
+            "type": "int|float",
+            "default_value": 100,
+            "doc": "The maximum value of the metric indicator"
+          },
+          {
+            "name": "format",
+            "type": "str",
+            "doc": "The format to use when displaying the value.<br/>This uses the <code>printf</code> syntax."
+          },
+          {
+            "name": "format_delta",
+            "type": "str",
+            "doc": "The format to use when displaying the delta value.<br/>This uses the <code>printf</code> syntax."
+          }
+        ]
+      }
+    ],
     ["login", {
         "inherits": ["shared"],
         "properties": [

+ 96 - 0
tests/gui/e2e/with_action/test_metric_indicator.py

@@ -0,0 +1,96 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+import inspect
+from importlib import util
+
+import pytest
+
+if util.find_spec("playwright"):
+  from playwright._impl._page import Page
+
+from taipy.gui import Gui
+
+
+@pytest.mark.teste2e
+def test_has_default_value(page: Page, gui: Gui, helpers):
+  default_value = 100
+  page_md = """
+<|{default_value}|metric|>
+"""
+  gui._set_frame(inspect.currentframe())
+  gui.add_page(name="test", page=page_md)
+  helpers.run_e2e(gui)
+  page.goto("./test")
+  page.wait_for_selector(".plot-container")
+  events_list = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[local-name()='text']")
+  gauge_value = events_list.nth(0).text_content()
+  assert gauge_value == "100"
+
+@pytest.mark.teste2e
+def test_show_increase_delta_value(page: Page, gui: Gui, helpers):
+  default_value = 100
+  page_md = """
+<|{default_value}|metric|delta=20|type=linear|>
+"""
+  gui._set_frame(inspect.currentframe())
+  gui.add_page(name="test", page=page_md)
+  helpers.run_e2e(gui)
+  page.goto("./test")
+  page.wait_for_selector(".plot-container")
+  events_list = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[local-name()='text']")
+  delta_value = events_list.nth(1).text_content()
+  assert delta_value == "▲20"
+
+@pytest.mark.teste2e
+def test_show_decrease_delta_value(page: Page, gui: Gui, helpers):
+  default_value = 100
+  delta_value = -20
+  page_md = """
+<|{default_value}|metric|delta={delta_value}|type=linear|>
+"""
+  gui._set_frame(inspect.currentframe())
+  gui.add_page(name="test", page=page_md)
+  helpers.run_e2e(gui)
+  page.goto("./test")
+  page.wait_for_selector(".plot-container")
+  events_list = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[local-name()='text']")
+  delta_value = events_list.nth(1).text_content()
+  assert delta_value == "▼−20"
+
+@pytest.mark.teste2es
+def test_show_linear_chart(page: Page, gui: Gui, helpers):
+  default_value = 100
+  page_md = """
+<|{default_value}|metric|delta=-20|type=linear|>
+"""
+  gui._set_frame(inspect.currentframe())
+  gui.add_page(name="test", page=page_md)
+  helpers.run_e2e(gui)
+  page.goto("./test")
+  page.wait_for_selector(".plot-container")
+  chart = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='bullet']")
+  assert chart.is_visible()
+
+@pytest.mark.teste2es
+def test_show_circular_chart_as_default_type(page: Page, gui: Gui, helpers):
+  default_value = 100
+  page_md = """
+<|{default_value}|metric|>
+"""
+  gui._set_frame(inspect.currentframe())
+  gui.add_page(name="test", page=page_md)
+  helpers.run_e2e(gui)
+  page.goto("./test")
+  page.wait_for_selector(".plot-container")
+  chart = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='angular']")
+  assert chart.is_visible()
+
+