mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-13 13:11:37 +01:00
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:
parent
e430ab1999
commit
f18ae015de
@ -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;
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useAppState } from "../../../state/common/StateContext";
|
import { useAppState } from "../../../state/common/StateContext";
|
||||||
@ -8,7 +9,7 @@ interface SpinnerProps {
|
|||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Spinner: FC<SpinnerProps> = ({ containerStyles = {}, message }) => {
|
const Spinner: FC<SpinnerProps> = ({ containerStyles, message }) => {
|
||||||
const { isDarkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -17,7 +18,7 @@ const Spinner: FC<SpinnerProps> = ({ containerStyles = {}, message }) => {
|
|||||||
"vm-spinner": true,
|
"vm-spinner": true,
|
||||||
"vm-spinner_dark": isDarkTheme,
|
"vm-spinner_dark": isDarkTheme,
|
||||||
})}
|
})}
|
||||||
style={containerStyles && {}}
|
style={containerStyles}
|
||||||
>
|
>
|
||||||
<div className="half-circle-spinner">
|
<div className="half-circle-spinner">
|
||||||
<div className="circle circle-1"></div>
|
<div className="circle circle-1"></div>
|
||||||
|
@ -24,6 +24,7 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
|||||||
import { QueryStats } from "../../../api/types";
|
import { QueryStats } from "../../../api/types";
|
||||||
import { usePrettifyQuery } from "./hooks/usePrettifyQuery";
|
import { usePrettifyQuery } from "./hooks/usePrettifyQuery";
|
||||||
import QueryHistory from "../QueryHistory/QueryHistory";
|
import QueryHistory from "../QueryHistory/QueryHistory";
|
||||||
|
import AnomalyConfig from "../../../components/ExploreAnomaly/AnomalyConfig";
|
||||||
|
|
||||||
export interface QueryConfiguratorProps {
|
export interface QueryConfiguratorProps {
|
||||||
queryErrors: string[];
|
queryErrors: string[];
|
||||||
@ -37,6 +38,7 @@ export interface QueryConfiguratorProps {
|
|||||||
prettify?: boolean;
|
prettify?: boolean;
|
||||||
autocomplete?: boolean;
|
autocomplete?: boolean;
|
||||||
traceQuery?: boolean;
|
traceQuery?: boolean;
|
||||||
|
anomalyConfig?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,6 +255,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
|
|||||||
<AdditionalSettings hideButtons={hideButtons}/>
|
<AdditionalSettings hideButtons={hideButtons}/>
|
||||||
<div className="vm-query-configurator-settings__buttons">
|
<div className="vm-query-configurator-settings__buttons">
|
||||||
<QueryHistory handleSelectQuery={handleSelectHistory}/>
|
<QueryHistory handleSelectQuery={handleSelectHistory}/>
|
||||||
|
{hideButtons?.anomalyConfig && <AnomalyConfig/>}
|
||||||
{!hideButtons?.addQuery && stateQuery.length < MAX_QUERY_FIELDS && (
|
{!hideButtons?.addQuery && stateQuery.length < MAX_QUERY_FIELDS && (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
@ -87,7 +87,7 @@ const ExploreAnomaly: FC = () => {
|
|||||||
setHideError={setHideError}
|
setHideError={setHideError}
|
||||||
stats={queryStats}
|
stats={queryStats}
|
||||||
onRunQuery={handleRunQuery}
|
onRunQuery={handleRunQuery}
|
||||||
hideButtons={{ addQuery: true, prettify: true, autocomplete: true, traceQuery: true }}
|
hideButtons={{ addQuery: true, prettify: true, autocomplete: true, traceQuery: true, anomalyConfig: true }}
|
||||||
/>
|
/>
|
||||||
{isLoading && <Spinner/>}
|
{isLoading && <Spinner/>}
|
||||||
{(!hideError && error) && <Alert variant="error">{error}</Alert>}
|
{(!hideError && error) && <Alert variant="error">{error}</Alert>}
|
||||||
|
Loading…
Reference in New Issue
Block a user