vmui/vmanomaly: add download config button (#6231)

This pull request adds a button to the vmanomaly ui that opens a modal
window for viewing and downloading the config file.

<img width="610" alt="button"
src="https://github.com/VictoriaMetrics/VictoriaMetrics/assets/29711459/0132b178-eb73-4272-8144-be7ed2a8dcaf">
<img height="300" alt="error"
src="https://github.com/VictoriaMetrics/VictoriaMetrics/assets/29711459/6d9f2627-77d7-4ce6-b73b-542ce1bbc999">
<img height="300" alt="modal"
src="https://github.com/VictoriaMetrics/VictoriaMetrics/assets/29711459/680bffdd-d6a3-445e-bd48-8f0feb30016e">

(cherry picked from commit 37c22ee053)
This commit is contained in:
Yury Molodov 2024-05-13 12:25:31 +02:00 committed by hagen1778
parent e430ab1999
commit f18ae015de
No known key found for this signature in database
GPG key ID: 3BF75F3741CA9640
5 changed files with 189 additions and 4 deletions

View file

@ -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<string>("");
const [downloadUrl, setDownloadUrl] = useState<string>("");
const [error, setError] = useState<string>("");
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 (
<>
<Button
color="secondary"
variant="outlined"
onClick={handleOpenModal}
>
Open Config
</Button>
{isModalOpen && (
<Modal
title="Download config"
onClose={setCloseModal}
>
<div
className={classNames({
"vm-anomaly-config": true,
"vm-anomaly-config_mobile": isMobile,
})}
>
{isLoading && (
<Spinner
containerStyles={{ position: "relative" }}
message={"Loading config..."}
/>
)}
{!isLoading && error && (
<div className="vm-anomaly-config-error">
<div className="vm-anomaly-config-error__icon"><ErrorIcon/></div>
<h3 className="vm-anomaly-config-error__title">Cannot download config</h3>
<p className="vm-anomaly-config-error__text">{error}</p>
</div>
)}
{!isLoading && textConfig && (
<TextField
value={textConfig}
label={"config.yaml"}
type="textarea"
disabled={true}
/>
)}
<div className="vm-anomaly-config-footer">
{downloadUrl && (
<a
href={downloadUrl}
download={"config.yaml"}
>
<Button
variant="contained"
startIcon={<DownloadIcon/>}
>
download
</Button>
</a>
)}
</div>
</div>
</Modal>
)}
</>
);
};
export default AnomalyConfig;

View file

@ -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;
}
}

View file

@ -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<SpinnerProps> = ({ containerStyles = {}, message }) => {
const Spinner: FC<SpinnerProps> = ({ containerStyles, message }) => {
const { isDarkTheme } = useAppState();
return (
@ -17,7 +18,7 @@ const Spinner: FC<SpinnerProps> = ({ containerStyles = {}, message }) => {
"vm-spinner": true,
"vm-spinner_dark": isDarkTheme,
})}
style={containerStyles && {}}
style={containerStyles}
>
<div className="half-circle-spinner">
<div className="circle circle-1"></div>

View file

@ -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<QueryConfiguratorProps> = ({
<AdditionalSettings hideButtons={hideButtons}/>
<div className="vm-query-configurator-settings__buttons">
<QueryHistory handleSelectQuery={handleSelectHistory}/>
{hideButtons?.anomalyConfig && <AnomalyConfig/>}
{!hideButtons?.addQuery && stateQuery.length < MAX_QUERY_FIELDS && (
<Button
variant="outlined"

View file

@ -87,7 +87,7 @@ const ExploreAnomaly: FC = () => {
setHideError={setHideError}
stats={queryStats}
onRunQuery={handleRunQuery}
hideButtons={{ addQuery: true, prettify: true, autocomplete: true, traceQuery: true }}
hideButtons={{ addQuery: true, prettify: true, autocomplete: true, traceQuery: true, anomalyConfig: true }}
/>
{isLoading && <Spinner/>}
{(!hideError && error) && <Alert variant="error">{error}</Alert>}