diff --git a/web/html/src/manager/storybook/index.ts b/web/html/src/manager/storybook/index.ts
new file mode 100644
index 000000000000..e6454cf7074d
--- /dev/null
+++ b/web/html/src/manager/storybook/index.ts
@@ -0,0 +1,3 @@
+export default {
+ storybook: () => import("./storybook.renderer"),
+};
diff --git a/web/html/src/manager/storybook/layout.module.less b/web/html/src/manager/storybook/layout.module.less
new file mode 100644
index 000000000000..45efd81ba4ad
--- /dev/null
+++ b/web/html/src/manager/storybook/layout.module.less
@@ -0,0 +1,29 @@
+:global(.old-theme),
+:global(.new-theme) {
+ .header {
+ padding: 8px 16px;
+ background: #eee;
+ }
+
+ .section {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 16px;
+ border: 1px solid #eee;
+
+ &:not(:last-child) {
+ margin-bottom: 16px;
+ }
+ }
+
+ .striped {
+ background: repeating-linear-gradient(-45deg, #fff, #fff 10px, #eee 10px, #eee 12px);
+ }
+
+ .row {
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+ }
+}
diff --git a/web/html/src/manager/storybook/layout.tsx b/web/html/src/manager/storybook/layout.tsx
new file mode 100644
index 000000000000..1f3481c3a800
--- /dev/null
+++ b/web/html/src/manager/storybook/layout.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+
+import styles from "./layout.module.less";
+
+type Props = {
+ children?: React.ReactNode;
+};
+
+export const StorySection = (props: Props) => {
+ return (
+ <>
+
{props.children}
+ >
+ );
+};
+
+export const StripedStorySection = (props: Props) => {
+ return (
+ <>
+ {props.children}
+ >
+ );
+};
+
+type RowProps = {
+ children?: React.ReactNode;
+};
+
+export const StoryRow = (props: RowProps) => {
+ return {props.children}
;
+};
diff --git a/web/html/src/manager/storybook/stories.generated.ts b/web/html/src/manager/storybook/stories.generated.ts
new file mode 100644
index 000000000000..bb56637d0967
--- /dev/null
+++ b/web/html/src/manager/storybook/stories.generated.ts
@@ -0,0 +1,370 @@
+/**
+ * NB! This is a generated file!
+ * Any changes you make here will be lost.
+ * See: web/html/src/build/plugins/generate-stories-plugin.js
+ */
+
+/* eslint-disable */
+
+import components_action_ActionStatus_stories_tsx_component from "components/action/ActionStatus.stories.tsx";
+import components_action_ActionStatus_stories_tsx_raw from "components/action/ActionStatus.stories.tsx?raw";
+
+export const components_action_ActionStatus_stories_tsx = {
+ path: "components/action/ActionStatus.stories.tsx",
+ title: "ActionStatus.stories.tsx",
+ groupName: "action",
+ component: components_action_ActionStatus_stories_tsx_component,
+ raw: components_action_ActionStatus_stories_tsx_raw,
+};
+
+import components_buttons_stories_tsx_component from "components/buttons.stories.tsx";
+import components_buttons_stories_tsx_raw from "components/buttons.stories.tsx?raw";
+
+export const components_buttons_stories_tsx = {
+ path: "components/buttons.stories.tsx",
+ title: "buttons.stories.tsx",
+ groupName: "components",
+ component: components_buttons_stories_tsx_component,
+ raw: components_buttons_stories_tsx_raw,
+};
+
+import components_datetime_DateTimePicker_stories_tsx_component from "components/datetime/DateTimePicker.stories.tsx";
+import components_datetime_DateTimePicker_stories_tsx_raw from "components/datetime/DateTimePicker.stories.tsx?raw";
+
+export const components_datetime_DateTimePicker_stories_tsx = {
+ path: "components/datetime/DateTimePicker.stories.tsx",
+ title: "DateTimePicker.stories.tsx",
+ groupName: "datetime",
+ component: components_datetime_DateTimePicker_stories_tsx_component,
+ raw: components_datetime_DateTimePicker_stories_tsx_raw,
+};
+
+import components_datetime_FromNow_stories_tsx_component from "components/datetime/FromNow.stories.tsx";
+import components_datetime_FromNow_stories_tsx_raw from "components/datetime/FromNow.stories.tsx?raw";
+
+export const components_datetime_FromNow_stories_tsx = {
+ path: "components/datetime/FromNow.stories.tsx",
+ title: "FromNow.stories.tsx",
+ groupName: "datetime",
+ component: components_datetime_FromNow_stories_tsx_component,
+ raw: components_datetime_FromNow_stories_tsx_raw,
+};
+
+import components_dialog_action_confirm_stories_tsx_component from "components/dialog/action-confirm.stories.tsx";
+import components_dialog_action_confirm_stories_tsx_raw from "components/dialog/action-confirm.stories.tsx?raw";
+
+export const components_dialog_action_confirm_stories_tsx = {
+ path: "components/dialog/action-confirm.stories.tsx",
+ title: "action-confirm.stories.tsx",
+ groupName: "dialog",
+ component: components_dialog_action_confirm_stories_tsx_component,
+ raw: components_dialog_action_confirm_stories_tsx_raw,
+};
+
+import components_dialog_delete_stories_tsx_component from "components/dialog/delete.stories.tsx";
+import components_dialog_delete_stories_tsx_raw from "components/dialog/delete.stories.tsx?raw";
+
+export const components_dialog_delete_stories_tsx = {
+ path: "components/dialog/delete.stories.tsx",
+ title: "delete.stories.tsx",
+ groupName: "dialog",
+ component: components_dialog_delete_stories_tsx_component,
+ raw: components_dialog_delete_stories_tsx_raw,
+};
+
+import components_input_check_Check_stories_tsx_component from "components/input/check/Check.stories.tsx";
+import components_input_check_Check_stories_tsx_raw from "components/input/check/Check.stories.tsx?raw";
+
+export const components_input_check_Check_stories_tsx = {
+ path: "components/input/check/Check.stories.tsx",
+ title: "Check.stories.tsx",
+ groupName: "check",
+ component: components_input_check_Check_stories_tsx_component,
+ raw: components_input_check_Check_stories_tsx_raw,
+};
+
+import components_input_datetime_DateTime_stories_tsx_component from "components/input/datetime/DateTime.stories.tsx";
+import components_input_datetime_DateTime_stories_tsx_raw from "components/input/datetime/DateTime.stories.tsx?raw";
+
+export const components_input_datetime_DateTime_stories_tsx = {
+ path: "components/input/datetime/DateTime.stories.tsx",
+ title: "DateTime.stories.tsx",
+ groupName: "datetime",
+ component: components_input_datetime_DateTime_stories_tsx_component,
+ raw: components_input_datetime_DateTime_stories_tsx_raw,
+};
+
+import components_input_form_multi_input_multiple_fields_stories_tsx_component from "components/input/form-multi-input/multiple-fields.stories.tsx";
+import components_input_form_multi_input_multiple_fields_stories_tsx_raw from "components/input/form-multi-input/multiple-fields.stories.tsx?raw";
+
+export const components_input_form_multi_input_multiple_fields_stories_tsx = {
+ path: "components/input/form-multi-input/multiple-fields.stories.tsx",
+ title: "multiple-fields.stories.tsx",
+ groupName: "form-multi-input",
+ component: components_input_form_multi_input_multiple_fields_stories_tsx_component,
+ raw: components_input_form_multi_input_multiple_fields_stories_tsx_raw,
+};
+
+import components_input_form_multi_input_single_field_stories_tsx_component from "components/input/form-multi-input/single-field.stories.tsx";
+import components_input_form_multi_input_single_field_stories_tsx_raw from "components/input/form-multi-input/single-field.stories.tsx?raw";
+
+export const components_input_form_multi_input_single_field_stories_tsx = {
+ path: "components/input/form-multi-input/single-field.stories.tsx",
+ title: "single-field.stories.tsx",
+ groupName: "form-multi-input",
+ component: components_input_form_multi_input_single_field_stories_tsx_component,
+ raw: components_input_form_multi_input_single_field_stories_tsx_raw,
+};
+
+import components_input_form_multi_input_table_fields_stories_tsx_component from "components/input/form-multi-input/table-fields.stories.tsx";
+import components_input_form_multi_input_table_fields_stories_tsx_raw from "components/input/form-multi-input/table-fields.stories.tsx?raw";
+
+export const components_input_form_multi_input_table_fields_stories_tsx = {
+ path: "components/input/form-multi-input/table-fields.stories.tsx",
+ title: "table-fields.stories.tsx",
+ groupName: "form-multi-input",
+ component: components_input_form_multi_input_table_fields_stories_tsx_component,
+ raw: components_input_form_multi_input_table_fields_stories_tsx_raw,
+};
+
+import components_input_form_Form_stories_tsx_component from "components/input/form/Form.stories.tsx";
+import components_input_form_Form_stories_tsx_raw from "components/input/form/Form.stories.tsx?raw";
+
+export const components_input_form_Form_stories_tsx = {
+ path: "components/input/form/Form.stories.tsx",
+ title: "Form.stories.tsx",
+ groupName: "form",
+ component: components_input_form_Form_stories_tsx_component,
+ raw: components_input_form_Form_stories_tsx_raw,
+};
+
+import components_input_password_Password_stories_tsx_component from "components/input/password/Password.stories.tsx";
+import components_input_password_Password_stories_tsx_raw from "components/input/password/Password.stories.tsx?raw";
+
+export const components_input_password_Password_stories_tsx = {
+ path: "components/input/password/Password.stories.tsx",
+ title: "Password.stories.tsx",
+ groupName: "password",
+ component: components_input_password_Password_stories_tsx_component,
+ raw: components_input_password_Password_stories_tsx_raw,
+};
+
+import components_input_radio_horizontal_open_stories_tsx_component from "components/input/radio/horizontal-open.stories.tsx";
+import components_input_radio_horizontal_open_stories_tsx_raw from "components/input/radio/horizontal-open.stories.tsx?raw";
+
+export const components_input_radio_horizontal_open_stories_tsx = {
+ path: "components/input/radio/horizontal-open.stories.tsx",
+ title: "horizontal-open.stories.tsx",
+ groupName: "radio",
+ component: components_input_radio_horizontal_open_stories_tsx_component,
+ raw: components_input_radio_horizontal_open_stories_tsx_raw,
+};
+
+import components_input_radio_horizontal_stories_tsx_component from "components/input/radio/horizontal.stories.tsx";
+import components_input_radio_horizontal_stories_tsx_raw from "components/input/radio/horizontal.stories.tsx?raw";
+
+export const components_input_radio_horizontal_stories_tsx = {
+ path: "components/input/radio/horizontal.stories.tsx",
+ title: "horizontal.stories.tsx",
+ groupName: "radio",
+ component: components_input_radio_horizontal_stories_tsx_component,
+ raw: components_input_radio_horizontal_stories_tsx_raw,
+};
+
+import components_input_radio_vertical_open_stories_tsx_component from "components/input/radio/vertical-open.stories.tsx";
+import components_input_radio_vertical_open_stories_tsx_raw from "components/input/radio/vertical-open.stories.tsx?raw";
+
+export const components_input_radio_vertical_open_stories_tsx = {
+ path: "components/input/radio/vertical-open.stories.tsx",
+ title: "vertical-open.stories.tsx",
+ groupName: "radio",
+ component: components_input_radio_vertical_open_stories_tsx_component,
+ raw: components_input_radio_vertical_open_stories_tsx_raw,
+};
+
+import components_input_radio_vertical_stories_tsx_component from "components/input/radio/vertical.stories.tsx";
+import components_input_radio_vertical_stories_tsx_raw from "components/input/radio/vertical.stories.tsx?raw";
+
+export const components_input_radio_vertical_stories_tsx = {
+ path: "components/input/radio/vertical.stories.tsx",
+ title: "vertical.stories.tsx",
+ groupName: "radio",
+ component: components_input_radio_vertical_stories_tsx_component,
+ raw: components_input_radio_vertical_stories_tsx_raw,
+};
+
+import components_input_range_Range_stories_tsx_component from "components/input/range/Range.stories.tsx";
+import components_input_range_Range_stories_tsx_raw from "components/input/range/Range.stories.tsx?raw";
+
+export const components_input_range_Range_stories_tsx = {
+ path: "components/input/range/Range.stories.tsx",
+ title: "Range.stories.tsx",
+ groupName: "range",
+ component: components_input_range_Range_stories_tsx_component,
+ raw: components_input_range_Range_stories_tsx_raw,
+};
+
+import components_input_select_async_stories_tsx_component from "components/input/select/async.stories.tsx";
+import components_input_select_async_stories_tsx_raw from "components/input/select/async.stories.tsx?raw";
+
+export const components_input_select_async_stories_tsx = {
+ path: "components/input/select/async.stories.tsx",
+ title: "async.stories.tsx",
+ groupName: "select",
+ component: components_input_select_async_stories_tsx_component,
+ raw: components_input_select_async_stories_tsx_raw,
+};
+
+import components_input_select_custom_stories_tsx_component from "components/input/select/custom.stories.tsx";
+import components_input_select_custom_stories_tsx_raw from "components/input/select/custom.stories.tsx?raw";
+
+export const components_input_select_custom_stories_tsx = {
+ path: "components/input/select/custom.stories.tsx",
+ title: "custom.stories.tsx",
+ groupName: "select",
+ component: components_input_select_custom_stories_tsx_component,
+ raw: components_input_select_custom_stories_tsx_raw,
+};
+
+import components_input_select_simple_stories_tsx_component from "components/input/select/simple.stories.tsx";
+import components_input_select_simple_stories_tsx_raw from "components/input/select/simple.stories.tsx?raw";
+
+export const components_input_select_simple_stories_tsx = {
+ path: "components/input/select/simple.stories.tsx",
+ title: "simple.stories.tsx",
+ groupName: "select",
+ component: components_input_select_simple_stories_tsx_component,
+ raw: components_input_select_simple_stories_tsx_raw,
+};
+
+import components_input_text_Text_stories_tsx_component from "components/input/text/Text.stories.tsx";
+import components_input_text_Text_stories_tsx_raw from "components/input/text/Text.stories.tsx?raw";
+
+export const components_input_text_Text_stories_tsx = {
+ path: "components/input/text/Text.stories.tsx",
+ title: "Text.stories.tsx",
+ groupName: "text",
+ component: components_input_text_Text_stories_tsx_component,
+ raw: components_input_text_Text_stories_tsx_raw,
+};
+
+import components_messages_severities_stories_tsx_component from "components/messages/severities.stories.tsx";
+import components_messages_severities_stories_tsx_raw from "components/messages/severities.stories.tsx?raw";
+
+export const components_messages_severities_stories_tsx = {
+ path: "components/messages/severities.stories.tsx",
+ title: "severities.stories.tsx",
+ groupName: "messages",
+ component: components_messages_severities_stories_tsx_component,
+ raw: components_messages_severities_stories_tsx_raw,
+};
+
+import components_messages_utilities_stories_tsx_component from "components/messages/utilities.stories.tsx";
+import components_messages_utilities_stories_tsx_raw from "components/messages/utilities.stories.tsx?raw";
+
+export const components_messages_utilities_stories_tsx = {
+ path: "components/messages/utilities.stories.tsx",
+ title: "utilities.stories.tsx",
+ groupName: "messages",
+ component: components_messages_utilities_stories_tsx_component,
+ raw: components_messages_utilities_stories_tsx_raw,
+};
+
+import components_panels_BootstrapPanel_stories_tsx_component from "components/panels/BootstrapPanel.stories.tsx";
+import components_panels_BootstrapPanel_stories_tsx_raw from "components/panels/BootstrapPanel.stories.tsx?raw";
+
+export const components_panels_BootstrapPanel_stories_tsx = {
+ path: "components/panels/BootstrapPanel.stories.tsx",
+ title: "BootstrapPanel.stories.tsx",
+ groupName: "panels",
+ component: components_panels_BootstrapPanel_stories_tsx_component,
+ raw: components_panels_BootstrapPanel_stories_tsx_raw,
+};
+
+import components_panels_TopPanel_stories_tsx_component from "components/panels/TopPanel.stories.tsx";
+import components_panels_TopPanel_stories_tsx_raw from "components/panels/TopPanel.stories.tsx?raw";
+
+export const components_panels_TopPanel_stories_tsx = {
+ path: "components/panels/TopPanel.stories.tsx",
+ title: "TopPanel.stories.tsx",
+ groupName: "panels",
+ component: components_panels_TopPanel_stories_tsx_component,
+ raw: components_panels_TopPanel_stories_tsx_raw,
+};
+
+import components_toastr_toastr_stories_tsx_component from "components/toastr/toastr.stories.tsx";
+import components_toastr_toastr_stories_tsx_raw from "components/toastr/toastr.stories.tsx?raw";
+
+export const components_toastr_toastr_stories_tsx = {
+ path: "components/toastr/toastr.stories.tsx",
+ title: "toastr.stories.tsx",
+ groupName: "toastr",
+ component: components_toastr_toastr_stories_tsx_component,
+ raw: components_toastr_toastr_stories_tsx_raw,
+};
+
+import components_tree_tree_stories_tsx_component from "components/tree/tree.stories.tsx";
+import components_tree_tree_stories_tsx_raw from "components/tree/tree.stories.tsx?raw";
+
+export const components_tree_tree_stories_tsx = {
+ path: "components/tree/tree.stories.tsx",
+ title: "tree.stories.tsx",
+ groupName: "tree",
+ component: components_tree_tree_stories_tsx_component,
+ raw: components_tree_tree_stories_tsx_raw,
+};
+
+import components_utils_HelpIcon_stories_tsx_component from "components/utils/HelpIcon.stories.tsx";
+import components_utils_HelpIcon_stories_tsx_raw from "components/utils/HelpIcon.stories.tsx?raw";
+
+export const components_utils_HelpIcon_stories_tsx = {
+ path: "components/utils/HelpIcon.stories.tsx",
+ title: "HelpIcon.stories.tsx",
+ groupName: "utils",
+ component: components_utils_HelpIcon_stories_tsx_component,
+ raw: components_utils_HelpIcon_stories_tsx_raw,
+};
+
+import components_utils_HelpLink_stories_tsx_component from "components/utils/HelpLink.stories.tsx";
+import components_utils_HelpLink_stories_tsx_raw from "components/utils/HelpLink.stories.tsx?raw";
+
+export const components_utils_HelpLink_stories_tsx = {
+ path: "components/utils/HelpLink.stories.tsx",
+ title: "HelpLink.stories.tsx",
+ groupName: "utils",
+ component: components_utils_HelpLink_stories_tsx_component,
+ raw: components_utils_HelpLink_stories_tsx_raw,
+};
+
+import components_utils_loading_simple_stories_tsx_component from "components/utils/loading/simple.stories.tsx";
+import components_utils_loading_simple_stories_tsx_raw from "components/utils/loading/simple.stories.tsx?raw";
+
+export const components_utils_loading_simple_stories_tsx = {
+ path: "components/utils/loading/simple.stories.tsx",
+ title: "simple.stories.tsx",
+ groupName: "loading",
+ component: components_utils_loading_simple_stories_tsx_component,
+ raw: components_utils_loading_simple_stories_tsx_raw,
+};
+
+import components_utils_loading_text_border_stories_tsx_component from "components/utils/loading/text-border.stories.tsx";
+import components_utils_loading_text_border_stories_tsx_raw from "components/utils/loading/text-border.stories.tsx?raw";
+
+export const components_utils_loading_text_border_stories_tsx = {
+ path: "components/utils/loading/text-border.stories.tsx",
+ title: "text-border.stories.tsx",
+ groupName: "loading",
+ component: components_utils_loading_text_border_stories_tsx_component,
+ raw: components_utils_loading_text_border_stories_tsx_raw,
+};
+
+import components_utils_loading_text_stories_tsx_component from "components/utils/loading/text.stories.tsx";
+import components_utils_loading_text_stories_tsx_raw from "components/utils/loading/text.stories.tsx?raw";
+
+export const components_utils_loading_text_stories_tsx = {
+ path: "components/utils/loading/text.stories.tsx",
+ title: "text.stories.tsx",
+ groupName: "loading",
+ component: components_utils_loading_text_stories_tsx_component,
+ raw: components_utils_loading_text_stories_tsx_raw,
+};
\ No newline at end of file
diff --git a/web/html/src/manager/storybook/stories.ts b/web/html/src/manager/storybook/stories.ts
new file mode 100644
index 000000000000..3580ea03c530
--- /dev/null
+++ b/web/html/src/manager/storybook/stories.ts
@@ -0,0 +1,5 @@
+import * as generatedStories from "./stories.generated";
+
+const storyGroups = Object.groupBy(Object.values(generatedStories), (item) => item.groupName);
+
+export default Object.entries(storyGroups).map(([title, stories]) => ({ title, stories }));
diff --git a/web/html/src/manager/storybook/storybook.module.less b/web/html/src/manager/storybook/storybook.module.less
new file mode 100644
index 000000000000..da4763d8a6f2
--- /dev/null
+++ b/web/html/src/manager/storybook/storybook.module.less
@@ -0,0 +1,29 @@
+:global(.old-theme),
+:global(.new-theme) {
+ .header {
+ // position: sticky;
+ top: 8px;
+ padding-bottom: 8px;
+ background: #fff;
+ z-index: 1;
+ }
+
+ .story {
+ display: flex;
+ gap: 16px;
+
+ > div {
+ flex: 1 0 auto;
+ }
+
+ > pre {
+ flex: 1 1 auto;
+ overflow: auto;
+ max-width: 50%;
+
+ code {
+ white-space: pre;
+ }
+ }
+ }
+}
diff --git a/web/html/src/manager/storybook/storybook.renderer.tsx b/web/html/src/manager/storybook/storybook.renderer.tsx
new file mode 100644
index 000000000000..b0d3cb350413
--- /dev/null
+++ b/web/html/src/manager/storybook/storybook.renderer.tsx
@@ -0,0 +1,16 @@
+import * as React from "react";
+
+import SpaRenderer from "core/spa/spa-renderer";
+
+import { MessagesContainer } from "components/toastr";
+
+import { Storybook } from "./storybook";
+
+export const renderer = (id: string) =>
+ SpaRenderer.renderNavigationReact(
+ <>
+
+
+ >,
+ document.getElementById(id)
+ );
diff --git a/web/html/src/manager/storybook/storybook.tsx b/web/html/src/manager/storybook/storybook.tsx
new file mode 100644
index 000000000000..a11d3671c195
--- /dev/null
+++ b/web/html/src/manager/storybook/storybook.tsx
@@ -0,0 +1,112 @@
+import { Fragment, useEffect, useState } from "react";
+
+import debugUtils from "core/debugUtils";
+
+import { Button } from "components/buttons";
+import { IconTag } from "components/icontag";
+
+import { StoryRow } from "./layout";
+import stories from "./stories";
+import styles from "./storybook.module.less";
+
+const STORAGE_KEY = "storybook-show-code";
+
+export const Storybook = () => {
+ const [_hash, setHash] = useState(window.location.hash);
+ const hash = _hash.replace(/^#/, "");
+ const normalize = (input: string = "") => input.replaceAll(" ", "-").toLowerCase();
+
+ const activeTabHash = normalize(hash) || normalize(stories[0]?.title);
+
+ const [, _invalidate] = useState(0);
+ const invalidate = () => _invalidate((ii) => ii + 1);
+
+ const [showCode, _setShowCode] = useState(!!localStorage.getItem(STORAGE_KEY));
+ const setShowCode = (value: boolean) => {
+ _setShowCode(value);
+ if (value) {
+ localStorage.setItem(STORAGE_KEY, "true");
+ } else {
+ localStorage.removeItem(STORAGE_KEY);
+ }
+ };
+
+ useEffect(() => {
+ const listener = () => setHash(window.location.hash);
+ window.addEventListener("hashchange", listener);
+ return () => {
+ window.removeEventListener("hashchange", listener);
+ };
+ }, []);
+
+ return (
+ <>
+
+
+
+ {t("Development debugging page")}
+
+
{t("This is a hidden page used by developers, if you found it by accident, good job!")}
+
+ {document.body.className}
+
+
+
+
+
+
+
+
+ {stories
+ .sort((a, b) => a.title.localeCompare(b.title))
+ .map((item) => {
+ const tabHash = normalize(item.title);
+ return (
+ -
+ {item.title}
+
+ );
+ })}
+
+
+
+ {stories.map((group) => (
+
+ {normalize(group.title) === activeTabHash && group.stories?.map((item) => (
+
+
+ {item.title}
+
+
+
{item.component ? : null}
+ {showCode ? (
+
+ {item.raw}
+
+ ) : null}
+
+
+
+ ))}
+
+ ))}
+ >
+ );
+};