diff --git a/app/vmui/packages/vmui/src/components/ExploreAnomaly/AnomalyConfig.tsx b/app/vmui/packages/vmui/src/components/ExploreAnomaly/AnomalyConfig.tsx new file mode 100644 index 0000000000..ca9a90fc52 --- /dev/null +++ b/app/vmui/packages/vmui/src/components/ExploreAnomaly/AnomalyConfig.tsx @@ -0,0 +1,120 @@ +import React, { FC, useState } from "preact/compat"; +import Button from "../Main/Button/Button"; +import TextField from "../Main/TextField/TextField"; +import Modal from "../Main/Modal/Modal"; +import Spinner from "../Main/Spinner/Spinner"; +import { DownloadIcon, ErrorIcon } from "../Main/Icons"; +import useBoolean from "../../hooks/useBoolean"; +import useDeviceDetect from "../../hooks/useDeviceDetect"; +import { useAppState } from "../../state/common/StateContext"; +import classNames from "classnames"; +import "./style.scss"; + +const AnomalyConfig: FC = () => { + const { serverUrl } = useAppState(); + const { isMobile } = useDeviceDetect(); + + const { + value: isModalOpen, + setTrue: setOpenModal, + setFalse: setCloseModal, + } = useBoolean(false); + + const [isLoading, setIsLoading] = useState(false); + const [textConfig, setTextConfig] = useState(""); + const [downloadUrl, setDownloadUrl] = useState(""); + const [error, setError] = useState(""); + + const fetchConfig = async () => { + setIsLoading(true); + try { + const url = `${serverUrl}/api/vmanomaly/config.yaml`; + const response = await fetch(url); + if (!response.ok) { + setError(` ${response.status} ${response.statusText}`); + } else { + const blob = await response.blob(); + const yamlAsString = await blob.text(); + setTextConfig(yamlAsString); + setDownloadUrl(URL.createObjectURL(blob)); + } + } catch (error) { + console.error(error); + setError(String(error)); + } + setIsLoading(false); + }; + + const handleOpenModal = () => { + setOpenModal(); + setError(""); + URL.revokeObjectURL(downloadUrl); + setTextConfig(""); + setDownloadUrl(""); + fetchConfig(); + }; + + return ( + <> + + {isModalOpen && ( + +
+ {isLoading && ( + + )} + {!isLoading && error && ( +
+
+

Cannot download config

+

{error}

+
+ )} + {!isLoading && textConfig && ( + + )} +
+ {downloadUrl && ( + + + + )} +
+
+
+ )} + + ); +}; + +export default AnomalyConfig; diff --git a/app/vmui/packages/vmui/src/components/ExploreAnomaly/style.scss b/app/vmui/packages/vmui/src/components/ExploreAnomaly/style.scss new file mode 100644 index 0000000000..72a43e3149 --- /dev/null +++ b/app/vmui/packages/vmui/src/components/ExploreAnomaly/style.scss @@ -0,0 +1,61 @@ +@use "src/styles/variables" as *; + +.vm-anomaly-config { + display: grid; + grid-template-rows: calc(($vh * 70) - 78px - ($padding-medium*3)) auto; + gap: $padding-global; + min-width: 400px; + max-width: 80vw; + min-height: 300px; + + &_mobile { + width: 100%; + max-width: none; + min-height: 100%; + grid-template-rows: calc(($vh * 100) - 78px - ($padding-global*3)) auto; + } + + textarea { + overflow: auto; + width: 100%; + height: 100%; + max-height: 900px; + } + + &-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + gap: $padding-small; + text-align: center; + + &__icon { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + margin-bottom: $padding-small; + color: $color-error; + } + + &__title { + font-size: $font-size-medium; + font-weight: bold; + } + + &__text { + max-width: 700px; + line-height: 1.3; + } + } + + &-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: $padding-small; + } +} diff --git a/app/vmui/packages/vmui/src/components/Main/Spinner/Spinner.tsx b/app/vmui/packages/vmui/src/components/Main/Spinner/Spinner.tsx index 05e997e36a..e19549a172 100644 --- a/app/vmui/packages/vmui/src/components/Main/Spinner/Spinner.tsx +++ b/app/vmui/packages/vmui/src/components/Main/Spinner/Spinner.tsx @@ -1,4 +1,5 @@ -import React, { CSSProperties, FC } from "preact/compat"; +import React, { FC } from "preact/compat"; +import { CSSProperties } from "react"; import "./style.scss"; import classNames from "classnames"; import { useAppState } from "../../../state/common/StateContext"; @@ -8,7 +9,7 @@ interface SpinnerProps { message?: string } -const Spinner: FC = ({ containerStyles = {}, message }) => { +const Spinner: FC = ({ containerStyles, message }) => { const { isDarkTheme } = useAppState(); return ( @@ -17,7 +18,7 @@ const Spinner: FC = ({ containerStyles = {}, message }) => { "vm-spinner": true, "vm-spinner_dark": isDarkTheme, })} - style={containerStyles && {}} + style={containerStyles} >
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx index bd687041e9..6ca90a9217 100644 --- a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx +++ b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx @@ -24,6 +24,7 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect"; import { QueryStats } from "../../../api/types"; import { usePrettifyQuery } from "./hooks/usePrettifyQuery"; import QueryHistory from "../QueryHistory/QueryHistory"; +import AnomalyConfig from "../../../components/ExploreAnomaly/AnomalyConfig"; export interface QueryConfiguratorProps { queryErrors: string[]; @@ -37,6 +38,7 @@ export interface QueryConfiguratorProps { prettify?: boolean; autocomplete?: boolean; traceQuery?: boolean; + anomalyConfig?: boolean; } } @@ -253,6 +255,7 @@ const QueryConfigurator: FC = ({
+ {hideButtons?.anomalyConfig && } {!hideButtons?.addQuery && stateQuery.length < MAX_QUERY_FIELDS && (