mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 22:19:02 +08:00
Merge branch 'develop' into release/v0.15.0
This commit is contained in:
commit
e6ce80213b
@ -511,13 +511,15 @@ else
|
||||
echo ""
|
||||
echo -e "🟢 Your frontend is running on http://localhost:3301"
|
||||
echo ""
|
||||
echo "ℹ️ By default, retention period is set to 7 days for logs and traces, and 30 days for metrics."
|
||||
echo -e "To change this, navigate to the General tab on the Settings page of SigNoz UI. For more details, refer to https://signoz.io/docs/userguide/retention-period \n"
|
||||
|
||||
echo "ℹ️ To bring down SigNoz and clean volumes : $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml down -v"
|
||||
|
||||
echo ""
|
||||
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
|
||||
echo ""
|
||||
echo "👉 Need help Getting Started?"
|
||||
echo "👉 Need help in Getting Started?"
|
||||
echo -e "Join us on Slack https://signoz.io/slack"
|
||||
echo ""
|
||||
echo -e "\n📨 Please share your email to receive support & updates about SigNoz!"
|
||||
|
@ -76,6 +76,7 @@
|
||||
"react-graph-vis": "^1.0.5",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-i18next": "^11.16.1",
|
||||
"react-intersection-observer": "9.4.1",
|
||||
"react-query": "^3.34.19",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
|
@ -8,6 +8,7 @@ import getChartData from 'lib/getChartData';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useQuery } from 'react-query';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
@ -39,6 +40,11 @@ function GridCardGraph({
|
||||
setLayout,
|
||||
onDragSelect,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const { ref: graphRef, inView: isGraphVisible } = useInView({
|
||||
threshold: 0,
|
||||
triggerOnce: true,
|
||||
});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [modal, setModal] = useState(false);
|
||||
@ -79,6 +85,7 @@ function GridCardGraph({
|
||||
}),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: isGraphVisible,
|
||||
refetchOnMount: false,
|
||||
onError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
@ -160,7 +167,7 @@ function GridCardGraph({
|
||||
|
||||
if (queryResponse.isError && !isEmptyLayout) {
|
||||
return (
|
||||
<span>
|
||||
<span ref={graphRef}>
|
||||
{getModals()}
|
||||
{!isEmpty(widget) && prevChartDataSetRef && (
|
||||
<>
|
||||
@ -192,7 +199,7 @@ function GridCardGraph({
|
||||
|
||||
if (prevChartDataSetRef?.labels === undefined && queryResponse.isLoading) {
|
||||
return (
|
||||
<span>
|
||||
<span ref={graphRef}>
|
||||
{!isEmpty(widget) && prevChartDataSetRef?.labels ? (
|
||||
<>
|
||||
<div className="drag-handle">
|
||||
@ -225,6 +232,7 @@ function GridCardGraph({
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={graphRef}
|
||||
onMouseOver={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
@ -260,7 +268,7 @@ function GridCardGraph({
|
||||
data={chartData}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '} // empty title to accommodate absolutely positioned widget header
|
||||
title={' '} // `empty title to accommodate absolutely positioned widget header
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
|
@ -10,4 +10,13 @@ export const tooltipStyles = {
|
||||
right: '0.313rem',
|
||||
color: themeColors.errorColor,
|
||||
};
|
||||
|
||||
export const errorTooltipPosition = 'top';
|
||||
|
||||
export const overlayStyles: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
};
|
||||
|
@ -5,11 +5,12 @@ import {
|
||||
ExclamationCircleOutlined,
|
||||
FullscreenOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Tooltip, Typography } from 'antd';
|
||||
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
||||
import { MenuItemType } from 'antd/es/menu/hooks/useItems';
|
||||
import Spinner from 'components/Spinner';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import history from 'lib/history';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -18,12 +19,16 @@ import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import { errorTooltipPosition, spinnerStyles, tooltipStyles } from './config';
|
||||
import {
|
||||
errorTooltipPosition,
|
||||
overlayStyles,
|
||||
spinnerStyles,
|
||||
tooltipStyles,
|
||||
} from './config';
|
||||
import {
|
||||
ArrowContainer,
|
||||
HeaderContainer,
|
||||
HeaderContentContainer,
|
||||
MenuItemContainer,
|
||||
} from './styles';
|
||||
|
||||
type TWidgetOptions = 'view' | 'edit' | 'delete' | string;
|
||||
@ -48,33 +53,45 @@ function WidgetHeader({
|
||||
errorMessage,
|
||||
}: IWidgetHeaderProps): JSX.Element {
|
||||
const [localHover, setLocalHover] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const onEditHandler = (): void => {
|
||||
const onEditHandler = useCallback((): void => {
|
||||
const widgetId = widget.id;
|
||||
history.push(
|
||||
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${widget.panelTypes}`,
|
||||
);
|
||||
};
|
||||
}, [widget.id, widget.panelTypes]);
|
||||
|
||||
const keyMethodMapping: {
|
||||
[K in TWidgetOptions]: { key: TWidgetOptions; method: VoidFunction };
|
||||
} = {
|
||||
view: {
|
||||
key: 'view',
|
||||
method: onView,
|
||||
} = useMemo(
|
||||
() => ({
|
||||
view: {
|
||||
key: 'view',
|
||||
method: onView,
|
||||
},
|
||||
edit: {
|
||||
key: 'edit',
|
||||
method: onEditHandler,
|
||||
},
|
||||
delete: {
|
||||
key: 'delete',
|
||||
method: onDelete,
|
||||
},
|
||||
}),
|
||||
[onDelete, onEditHandler, onView],
|
||||
);
|
||||
|
||||
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
|
||||
({ key }: { key: TWidgetOptions }): void => {
|
||||
const functionToCall = keyMethodMapping[key]?.method;
|
||||
if (functionToCall) {
|
||||
functionToCall();
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
edit: {
|
||||
key: 'edit',
|
||||
method: onEditHandler,
|
||||
},
|
||||
delete: {
|
||||
key: 'delete',
|
||||
method: onDelete,
|
||||
},
|
||||
};
|
||||
const onMenuItemSelectHandler = ({ key }: { key: TWidgetOptions }): void => {
|
||||
keyMethodMapping[key]?.method();
|
||||
};
|
||||
[keyMethodMapping],
|
||||
);
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const [deleteWidget, editWidget] = useComponentPermission(
|
||||
@ -82,49 +99,67 @@ function WidgetHeader({
|
||||
role,
|
||||
);
|
||||
|
||||
const menu = (
|
||||
<Menu onClick={onMenuItemSelectHandler}>
|
||||
<Menu.Item key={keyMethodMapping.view.key}>
|
||||
<MenuItemContainer>
|
||||
<span>View</span> <FullscreenOutlined />
|
||||
</MenuItemContainer>
|
||||
</Menu.Item>
|
||||
const menuList: MenuItemType[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: keyMethodMapping.view.key,
|
||||
icon: <FullscreenOutlined />,
|
||||
disabled: queryResponse.isLoading,
|
||||
label: 'View',
|
||||
},
|
||||
{
|
||||
key: keyMethodMapping.edit.key,
|
||||
icon: <EditFilled />,
|
||||
disabled: !editWidget,
|
||||
label: 'Edit',
|
||||
},
|
||||
{
|
||||
key: keyMethodMapping.delete.key,
|
||||
icon: <DeleteOutlined />,
|
||||
disabled: !deleteWidget,
|
||||
danger: true,
|
||||
label: 'Delete',
|
||||
},
|
||||
],
|
||||
[
|
||||
deleteWidget,
|
||||
editWidget,
|
||||
keyMethodMapping.delete.key,
|
||||
keyMethodMapping.edit.key,
|
||||
keyMethodMapping.view.key,
|
||||
queryResponse.isLoading,
|
||||
],
|
||||
);
|
||||
|
||||
{editWidget && (
|
||||
<Menu.Item key={keyMethodMapping.edit.key}>
|
||||
<MenuItemContainer>
|
||||
<span>Edit</span> <EditFilled />
|
||||
</MenuItemContainer>
|
||||
</Menu.Item>
|
||||
)}
|
||||
const onClickHandler = useCallback(() => {
|
||||
setIsOpen((open) => !open);
|
||||
}, []);
|
||||
|
||||
{deleteWidget && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key={keyMethodMapping.delete.key} danger>
|
||||
<MenuItemContainer>
|
||||
<span>Delete</span> <DeleteOutlined />
|
||||
</MenuItemContainer>
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
const menu = useMemo(
|
||||
() => ({
|
||||
items: menuList,
|
||||
onClick: onMenuItemSelectHandler,
|
||||
}),
|
||||
[menuList, onMenuItemSelectHandler],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlay={menu}
|
||||
trigger={['click']}
|
||||
overlayStyle={{ minWidth: 100 }}
|
||||
placement="bottom"
|
||||
>
|
||||
<>
|
||||
<div>
|
||||
<Dropdown
|
||||
destroyPopupOnHide
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
menu={menu}
|
||||
trigger={['click']}
|
||||
overlayStyle={overlayStyles}
|
||||
>
|
||||
<HeaderContainer
|
||||
onMouseOver={(): void => setLocalHover(true)}
|
||||
onMouseOut={(): void => setLocalHover(false)}
|
||||
hover={localHover}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
<HeaderContentContainer onClick={(e): void => e.preventDefault()}>
|
||||
<HeaderContentContainer>
|
||||
<Typography.Text style={{ maxWidth: '80%' }} ellipsis>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
@ -133,16 +168,16 @@ function WidgetHeader({
|
||||
</ArrowContainer>
|
||||
</HeaderContentContainer>
|
||||
</HeaderContainer>
|
||||
{queryResponse.isFetching && !queryResponse.isError && (
|
||||
<Spinner height="5vh" style={spinnerStyles} />
|
||||
)}
|
||||
{queryResponse.isError && (
|
||||
<Tooltip title={errorMessage} placement={errorTooltipPosition}>
|
||||
<ExclamationCircleOutlined style={tooltipStyles} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
</Dropdown>
|
||||
</Dropdown>
|
||||
{queryResponse.isFetching && !queryResponse.isError && (
|
||||
<Spinner height="5vh" style={spinnerStyles} />
|
||||
)}
|
||||
{queryResponse.isError && (
|
||||
<Tooltip title={errorMessage} placement={errorTooltipPosition}>
|
||||
<ExclamationCircleOutlined style={tooltipStyles} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,6 @@
|
||||
import { grey } from '@ant-design/colors';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const MenuItemContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const HeaderContainer = styled.div<{ hover: boolean }>`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
@ -47,6 +47,11 @@ function SearchFields({
|
||||
}
|
||||
}, [parsedQuery]);
|
||||
|
||||
const updateFieldsQuery = (updated: QueryFields[][]): void => {
|
||||
setFieldsQuery(updated);
|
||||
keyPrefixRef.current = hashCode(JSON.stringify(updated));
|
||||
};
|
||||
|
||||
const addSuggestedField = useCallback(
|
||||
(name: string): void => {
|
||||
if (!name) {
|
||||
@ -97,7 +102,7 @@ function SearchFields({
|
||||
keyPrefix={keyPrefixRef.current}
|
||||
onDropDownToggleHandler={onDropDownToggleHandler}
|
||||
fieldsQuery={fieldsQuery}
|
||||
setFieldsQuery={setFieldsQuery}
|
||||
setFieldsQuery={updateFieldsQuery}
|
||||
/>
|
||||
<SearchFieldsActionBar
|
||||
applyUpdate={applyUpdate}
|
||||
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { downloadObjectAsJson } from './util';
|
||||
import { cleardQueryData, downloadObjectAsJson } from './util';
|
||||
|
||||
function ShareModal({
|
||||
isJSONModalVisible,
|
||||
@ -51,6 +51,7 @@ function ShareModal({
|
||||
}
|
||||
}, [state.error, state.value, t]);
|
||||
|
||||
const selectedDataCleaned = cleardQueryData(selectedData);
|
||||
const GetFooterComponent = useMemo(() => {
|
||||
if (!isViewJSON) {
|
||||
return (
|
||||
@ -66,7 +67,7 @@ function ShareModal({
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(): void => {
|
||||
downloadObjectAsJson(selectedData, selectedData.title);
|
||||
downloadObjectAsJson(selectedDataCleaned, selectedData.title);
|
||||
}}
|
||||
>
|
||||
{t('download_json')}
|
||||
@ -79,7 +80,7 @@ function ShareModal({
|
||||
{t('copy_to_clipboard')}
|
||||
</Button>
|
||||
);
|
||||
}, [isViewJSON, jsonValue, selectedData, setCopy, t]);
|
||||
}, [isViewJSON, jsonValue, selectedData, selectedDataCleaned, setCopy, t]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
|
||||
export function downloadObjectAsJson(
|
||||
exportObj: unknown,
|
||||
exportName: string,
|
||||
@ -12,3 +14,18 @@ export function downloadObjectAsJson(
|
||||
downloadAnchorNode.click();
|
||||
downloadAnchorNode.remove();
|
||||
}
|
||||
|
||||
export function cleardQueryData(param: DashboardData): DashboardData {
|
||||
return {
|
||||
...param,
|
||||
widgets: param.widgets?.map((widget) => ({
|
||||
...widget,
|
||||
queryData: {
|
||||
...widget.queryData,
|
||||
data: {
|
||||
queryData: [],
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
@ -37,8 +37,11 @@ function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const isDisabled = useMemo(
|
||||
() => disabled || globalTime.isAutoRefreshDisabled,
|
||||
[globalTime.isAutoRefreshDisabled, disabled],
|
||||
() =>
|
||||
disabled ||
|
||||
globalTime.isAutoRefreshDisabled ||
|
||||
globalTime.selectedTime === 'custom',
|
||||
[globalTime.isAutoRefreshDisabled, disabled, globalTime.selectedTime],
|
||||
);
|
||||
|
||||
const localStorageData = JSON.parse(get(DASHBOARD_TIME_IN_DURATION) || '{}');
|
||||
@ -132,6 +135,11 @@ function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element {
|
||||
[localStorageData, pathname],
|
||||
);
|
||||
|
||||
if (globalTime.selectedTime === 'custom') {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="bottomLeft"
|
||||
|
@ -88,6 +88,14 @@ function DateTimeSelection({
|
||||
return timeInterval;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTime === 'custom') {
|
||||
setRefreshButtonHidden(true);
|
||||
} else {
|
||||
setRefreshButtonHidden(false);
|
||||
}
|
||||
}, [selectedTime]);
|
||||
|
||||
const getDefaultTime = (pathName: string): Time => {
|
||||
const defaultSelectedOption = getDefaultOption(pathName);
|
||||
|
||||
|
@ -11104,6 +11104,11 @@ react-i18next@^11.16.1:
|
||||
"@babel/runtime" "^7.14.5"
|
||||
html-parse-stringify "^3.0.1"
|
||||
|
||||
react-intersection-observer@9.4.1:
|
||||
version "9.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.4.1.tgz#4ccb21e16acd0b9cf5b28d275af7055bef878f6b"
|
||||
integrity sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==
|
||||
|
||||
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
Loading…
x
Reference in New Issue
Block a user