Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vmui: add storage for query history #5022

Merged
merged 4 commits into from Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
Expand Up @@ -430,3 +430,23 @@ export const ListIcon = () => (
<path d="M3 14h4v-4H3v4zm0 5h4v-4H3v4zM3 9h4V5H3v4zm5 5h13v-4H8v4zm0 5h13v-4H8v4zM8 5v4h13V5H8z"></path>
</svg>
);

export const StarBorderIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="m22 9.24-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"
></path>
</svg>
);

export const StarIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path>
</svg>
);
1 change: 1 addition & 0 deletions app/vmui/packages/vmui/src/constants/graph.ts
@@ -1,6 +1,7 @@
import { GraphSize, SeriesItemStats } from "../types";

export const MAX_QUERY_FIELDS = 4;
export const MAX_QUERIES_HISTORY = 25;
export const DEFAULT_MAX_SERIES = {
table: 100,
chart: 20,
Expand Down
4 changes: 2 additions & 2 deletions app/vmui/packages/vmui/src/hooks/useFetchQuery.ts
Expand Up @@ -125,7 +125,7 @@ export const useFetchQuery = ({
}

isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
seriesLimit = isHistogramResult ? Infinity : Math.max(totalLength, defaultLimit);
seriesLimit = isHistogramResult ? Infinity : defaultLimit;
const freeTempSize = seriesLimit - tempData.length;
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
d.group = counter;
Expand All @@ -140,7 +140,7 @@ export const useFetchQuery = ({
counter++;
}

const limitText = `Showing ${seriesLimit} series out of ${totalLength} series due to performance reasons. Please narrow down the query, so it returns less series`;
const limitText = `Showing ${tempData.length} series out of ${totalLength} series due to performance reasons. Please narrow down the query, so it returns less series`;
setWarning(totalLength > seriesLimit ? limitText : "");
isDisplayChart ? setGraphData(tempData as MetricResult[]) : setLiveData(tempData as InstantMetricResult[]);
setTraces(tempTraces);
Expand Down
Expand Up @@ -2,7 +2,7 @@ import React, { FC, StateUpdater, useEffect, useState } from "preact/compat";
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
import AdditionalSettings from "../../../components/Configurators/AdditionalSettings/AdditionalSettings";
import usePrevious from "../../../hooks/usePrevious";
import { MAX_QUERY_FIELDS } from "../../../constants/graph";
import { MAX_QUERIES_HISTORY, MAX_QUERY_FIELDS } from "../../../constants/graph";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import { useTimeDispatch } from "../../../state/time/TimeStateContext";
import {
Expand All @@ -22,7 +22,7 @@ import { arrayEquals } from "../../../utils/array";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { QueryStats } from "../../../api/types";
import { usePrettifyQuery } from "./hooks/usePrettifyQuery";
import QueryHistoryList from "../QueryHistory/QueryHistoryList";
import QueryHistory from "../QueryHistory/QueryHistory";

export interface QueryConfiguratorProps {
queryErrors: string[];
Expand Down Expand Up @@ -66,7 +66,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
const newValues = !queryEqual && q ? [...h.values, q] : h.values;

// limit the history
if (newValues.length > 25) newValues.shift();
if (newValues.length > MAX_QUERIES_HISTORY) newValues.shift();

return {
index: h.values.length - Number(queryEqual),
Expand Down Expand Up @@ -243,10 +243,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
<div className="vm-query-configurator-settings">
<AdditionalSettings/>
<div className="vm-query-configurator-settings__buttons">
<QueryHistoryList
history={queryHistory}
handleSelectQuery={handleSelectHistory}
/>
<QueryHistory handleSelectQuery={handleSelectHistory}/>
{stateQuery.length < MAX_QUERY_FIELDS && (
<Button
variant="outlined"
Expand Down
@@ -0,0 +1,188 @@
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
import Button from "../../../components/Main/Button/Button";
import { ClockIcon, DeleteIcon } from "../../../components/Main/Icons";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import useBoolean from "../../../hooks/useBoolean";
import Modal from "../../../components/Main/Modal/Modal";
import Tabs from "../../../components/Main/Tabs/Tabs";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useEventListener from "../../../hooks/useEventListener";
import { useQueryState } from "../../../state/query/QueryStateContext";
import { getQueriesFromStorage } from "./utils";
import QueryHistoryItem from "./QueryHistoryItem";
import classNames from "classnames";
import "./style.scss";
import { saveToStorage } from "../../../utils/storage";
import { arrayEquals } from "../../../utils/array";

interface Props {
handleSelectQuery: (query: string, index: number) => void
}

export const HistoryTabTypes = {
session: "session",
storage: "saved",
favorite: "favorite",
};

export const historyTabs = [
{ label: "Session history", value: HistoryTabTypes.session },
{ label: "Saved history", value: HistoryTabTypes.storage },
{ label: "Favorite queries", value: HistoryTabTypes.favorite },
];

const QueryHistory: FC<Props> = ({ handleSelectQuery }) => {
const { queryHistory: historyState } = useQueryState();
const { isMobile } = useDeviceDetect();

const {
value: openModal,
setTrue: handleOpenModal,
setFalse: handleCloseModal,
} = useBoolean(false);

const [activeTab, setActiveTab] = useState(historyTabs[0].value);
const [historyStorage, setHistoryStorage] = useState(getQueriesFromStorage("QUERY_HISTORY"));
const [historyFavorites, setHistoryFavorites] = useState(getQueriesFromStorage("QUERY_FAVORITES"));

const historySession = useMemo(() => {
return historyState.map((h) => h.values.filter(q => q).reverse());
}, [historyState]);

const list = useMemo(() => {
switch (activeTab) {
case HistoryTabTypes.favorite:
return historyFavorites;
case HistoryTabTypes.storage:
return historyStorage;
default:
return historySession;
}
}, [activeTab, historyFavorites, historyStorage, historySession]);

const isNoData = list?.every(s => !s.length);

const noDataText = useMemo(() => {
switch (activeTab) {
case HistoryTabTypes.favorite:
return "Favorites queries are empty.\nTo see your favorites, mark a query as a favorite.";
default:
return "Query history is empty.\nTo see the history, please make a query.";
}
}, [activeTab]);

const handleRunQuery = (group: number) => (value: string) => {
handleSelectQuery(value, group);
handleCloseModal();
};

const handleToggleFavorite = (value: string, isFavorite: boolean) => {
setHistoryFavorites((prev) => {
const values = prev[0] || [];
if (isFavorite) return [values.filter(v => v !== value)];
if (!isFavorite && !values.includes(value)) return [[...values, value]];
return prev;
});
};

const updateStageHistory = () => {
setHistoryStorage(getQueriesFromStorage("QUERY_HISTORY"));
setHistoryFavorites(getQueriesFromStorage("QUERY_FAVORITES"));
};

const handleClearStorage = () => {
saveToStorage("QUERY_HISTORY", "");
};

useEffect(() => {
const nextValue = historyFavorites[0] || [];
const prevValue = getQueriesFromStorage("QUERY_FAVORITES")[0] || [];
const isEqual = arrayEquals(nextValue, prevValue);
if (isEqual) return;
saveToStorage("QUERY_FAVORITES", JSON.stringify(historyFavorites));
}, [historyFavorites]);

useEventListener("storage", updateStageHistory);

return (
<>
<Tooltip title={"Show history"}>
<Button
color="primary"
variant="text"
onClick={handleOpenModal}
startIcon={<ClockIcon/>}
/>
</Tooltip>

{openModal && (
<Modal
title={"Query history"}
onClose={handleCloseModal}
>
<div
className={classNames({
"vm-query-history": true,
"vm-query-history_mobile": isMobile,
})}
>
<div
className={classNames({
"vm-query-history__tabs": true,
"vm-section-header__tabs": true,
"vm-query-history__tabs_mobile": isMobile,
})}
>
<Tabs
activeItem={activeTab}
items={historyTabs}
onChange={setActiveTab}
/>
</div>
<div className="vm-query-history-list">
{isNoData && <div className="vm-query-history-list__no-data">{noDataText}</div>}
{list.map((queries, group) => (
<div key={group}>
{list.length > 1 && (
<div
className={classNames({
"vm-query-history-list__group-title": true,
"vm-query-history-list__group-title_first": group === 0,
})}
>
Query {group + 1}
</div>
)}
{queries.map((query, index) => (
<QueryHistoryItem
key={index}
query={query}
favorites={historyFavorites.flat()}
onRun={handleRunQuery(group)}
onToggleFavorite={handleToggleFavorite}
/>
))}
</div>
))}
{(activeTab === HistoryTabTypes.storage) && !isNoData && (
<div className="vm-query-history-footer">
<Button
color="error"
variant="outlined"
size="small"
startIcon={<DeleteIcon/>}
onClick={handleClearStorage}
>
clear history
</Button>
</div>
)}
</div>
</div>
</Modal>
)}
</>
);
};

export default QueryHistory;
@@ -0,0 +1,65 @@
import React, { FC, useMemo } from "preact/compat";
import Button from "../../../components/Main/Button/Button";
import { CopyIcon, PlayCircleOutlineIcon, StarBorderIcon, StarIcon } from "../../../components/Main/Icons";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import "./style.scss";

interface Props {
query: string;
favorites: string[];
onRun: (query: string) => void;
onToggleFavorite: (query: string, isFavorite: boolean) => void;
}

const QueryHistoryItem: FC<Props> = ({ query, favorites, onRun, onToggleFavorite }) => {
const copyToClipboard = useCopyToClipboard();
const isFavorite = useMemo(() => favorites.includes(query), [query, favorites]);

const handleCopyQuery = async () => {
await copyToClipboard(query, "Query has been copied");
};

const handleRunQuery = () => {
onRun(query);
};

const handleToggleFavorite = () => {
onToggleFavorite(query, isFavorite);
};

return (
<div className="vm-query-history-item">
<span className="vm-query-history-item__value">{query}</span>
<div className="vm-query-history-item__buttons">
<Tooltip title={"Execute query"}>
<Button
size="small"
variant="text"
onClick={handleRunQuery}
startIcon={<PlayCircleOutlineIcon/>}
/>
</Tooltip>
<Tooltip title={"Copy query"}>
<Button
size="small"
variant="text"
onClick={handleCopyQuery}
startIcon={<CopyIcon/>}
/>
</Tooltip>
<Tooltip title={isFavorite ? "Remove Favorite" : "Add to Favorites"}>
<Button
size="small"
variant="text"
color={isFavorite ? "warning" : "primary"}
onClick={handleToggleFavorite}
startIcon={isFavorite ? <StarIcon/> : <StarBorderIcon/>}
/>
</Tooltip>
</div>
</div>
);
};

export default QueryHistoryItem;