mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 20:19:13 +08:00
feat: add 'create channel' option in channels list and refetch alert channels on opening the channels dropdown (#6416)
* feat: add channel creation option and auto-refresh channels list on dropdown open * chore: move inline styles to style.ts * fix: show the prompt to ask admin if the user doesn't have permissions * fix: display create channel option only if the user has permission * fix: prevent repeated new alert event logs + log new channel option inside dropdown
This commit is contained in:
parent
91bbeaf175
commit
a59e7b9dfb
@ -8,7 +8,7 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -83,16 +83,22 @@ function BasicInfo({
|
|||||||
window.open(ROUTES.CHANNELS_NEW, '_blank');
|
window.open(ROUTES.CHANNELS_NEW, '_blank');
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
const hasLoggedEvent = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!channels.loading && isNewRule) {
|
if (!channels.loading && isNewRule && !hasLoggedEvent.current) {
|
||||||
logEvent('Alert: New alert creation page visited', {
|
logEvent('Alert: New alert creation page visited', {
|
||||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||||
numberOfChannels: channels?.payload?.length,
|
numberOfChannels: channels?.payload?.length,
|
||||||
});
|
});
|
||||||
|
hasLoggedEvent.current = true;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [channels.payload, channels.loading]);
|
}, [channels.loading]);
|
||||||
|
|
||||||
|
const refetchChannels = async (): Promise<void> => {
|
||||||
|
await channels.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -197,7 +203,7 @@ function BasicInfo({
|
|||||||
{!shouldBroadCastToAllChannels && (
|
{!shouldBroadCastToAllChannels && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
noChannels
|
noChannels && !addNewChannelPermission
|
||||||
? 'No channels. Ask an admin to create a notification channel'
|
? 'No channels. Ask an admin to create a notification channel'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@ -212,10 +218,10 @@ function BasicInfo({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<ChannelSelect
|
<ChannelSelect
|
||||||
disabled={
|
onDropdownOpen={refetchChannels}
|
||||||
shouldBroadCastToAllChannels || noChannels || !!channels.loading
|
disabled={shouldBroadCastToAllChannels}
|
||||||
}
|
|
||||||
currentValue={alertDef.preferredChannels}
|
currentValue={alertDef.preferredChannels}
|
||||||
|
handleCreateNewChannels={handleCreateNewChannels}
|
||||||
channels={channels}
|
channels={channels}
|
||||||
onSelectChannels={(preferredChannels): void => {
|
onSelectChannels={(preferredChannels): void => {
|
||||||
setAlertDef({
|
setAlertDef({
|
||||||
|
@ -1,24 +1,33 @@
|
|||||||
import { Select } from 'antd';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { Select, Spin } from 'antd';
|
||||||
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { State } from 'hooks/useFetch';
|
import { State } from 'hooks/useFetch';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { PayloadProps } from 'types/api/channels/getAll';
|
import { PayloadProps } from 'types/api/channels/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import { StyledSelect } from './styles';
|
import { StyledCreateChannelOption, StyledSelect } from './styles';
|
||||||
|
|
||||||
export interface ChannelSelectProps {
|
export interface ChannelSelectProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
currentValue?: string[];
|
currentValue?: string[];
|
||||||
onSelectChannels: (s: string[]) => void;
|
onSelectChannels: (s: string[]) => void;
|
||||||
|
onDropdownOpen: () => void;
|
||||||
channels: State<PayloadProps | undefined>;
|
channels: State<PayloadProps | undefined>;
|
||||||
|
handleCreateNewChannels: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChannelSelect({
|
function ChannelSelect({
|
||||||
disabled,
|
disabled,
|
||||||
currentValue,
|
currentValue,
|
||||||
onSelectChannels,
|
onSelectChannels,
|
||||||
|
onDropdownOpen,
|
||||||
channels,
|
channels,
|
||||||
|
handleCreateNewChannels,
|
||||||
}: ChannelSelectProps): JSX.Element | null {
|
}: ChannelSelectProps): JSX.Element | null {
|
||||||
// init namespace for translations
|
// init namespace for translations
|
||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
@ -26,6 +35,10 @@ function ChannelSelect({
|
|||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
const handleChange = (value: string[]): void => {
|
const handleChange = (value: string[]): void => {
|
||||||
|
if (value.includes('add-new-channel')) {
|
||||||
|
handleCreateNewChannels();
|
||||||
|
return;
|
||||||
|
}
|
||||||
onSelectChannels(value);
|
onSelectChannels(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,9 +48,27 @@ function ChannelSelect({
|
|||||||
description: channels.errorMessage,
|
description: channels.errorMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
const [addNewChannelPermission] = useComponentPermission(
|
||||||
|
['add_new_channel'],
|
||||||
|
role,
|
||||||
|
);
|
||||||
|
|
||||||
const renderOptions = (): ReactNode[] => {
|
const renderOptions = (): ReactNode[] => {
|
||||||
const children: ReactNode[] = [];
|
const children: ReactNode[] = [];
|
||||||
|
|
||||||
|
if (!channels.loading && addNewChannelPermission) {
|
||||||
|
children.push(
|
||||||
|
<Select.Option key="add-new-channel" value="add-new-channel">
|
||||||
|
<StyledCreateChannelOption>
|
||||||
|
<PlusOutlined />
|
||||||
|
Create a new channel
|
||||||
|
</StyledCreateChannelOption>
|
||||||
|
</Select.Option>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
channels.loading ||
|
channels.loading ||
|
||||||
channels.payload === undefined ||
|
channels.payload === undefined ||
|
||||||
@ -56,6 +87,7 @@ function ChannelSelect({
|
|||||||
|
|
||||||
return children;
|
return children;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSelect
|
<StyledSelect
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -65,6 +97,12 @@ function ChannelSelect({
|
|||||||
placeholder={t('placeholder_channel_select')}
|
placeholder={t('placeholder_channel_select')}
|
||||||
data-testid="alert-channel-select"
|
data-testid="alert-channel-select"
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
|
notFoundContent={channels.loading && <Spin size="small" />}
|
||||||
|
onDropdownVisibleChange={(open): void => {
|
||||||
|
if (open) {
|
||||||
|
onDropdownOpen();
|
||||||
|
}
|
||||||
|
}}
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
handleChange(value as string[]);
|
handleChange(value as string[]);
|
||||||
}}
|
}}
|
||||||
|
@ -4,3 +4,10 @@ import styled from 'styled-components';
|
|||||||
export const StyledSelect = styled(Select)`
|
export const StyledSelect = styled(Select)`
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const StyledCreateChannelOption = styled.div`
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
`;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
function useFetch<PayloadProps, FunctionParams>(
|
function useFetch<PayloadProps, FunctionParams>(
|
||||||
@ -10,7 +10,7 @@ function useFetch<PayloadProps, FunctionParams>(
|
|||||||
(arg0: any): Promise<SuccessResponse<PayloadProps> | ErrorResponse>;
|
(arg0: any): Promise<SuccessResponse<PayloadProps> | ErrorResponse>;
|
||||||
},
|
},
|
||||||
param?: FunctionParams,
|
param?: FunctionParams,
|
||||||
): State<PayloadProps | undefined> {
|
): State<PayloadProps | undefined> & { refetch: () => Promise<void> } {
|
||||||
const [state, setStates] = useState<State<PayloadProps | undefined>>({
|
const [state, setStates] = useState<State<PayloadProps | undefined>>({
|
||||||
loading: true,
|
loading: true,
|
||||||
success: null,
|
success: null,
|
||||||
@ -19,37 +19,28 @@ function useFetch<PayloadProps, FunctionParams>(
|
|||||||
payload: undefined,
|
payload: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadingRef = useRef(0);
|
const fetchData = useCallback(async (): Promise<void> => {
|
||||||
|
setStates((prev) => ({ ...prev, loading: true }));
|
||||||
useEffect(() => {
|
|
||||||
try {
|
try {
|
||||||
(async (): Promise<void> => {
|
const response = await functions(param);
|
||||||
if (state.loading) {
|
|
||||||
const response = await functions(param);
|
|
||||||
|
|
||||||
if (loadingRef.current === 0) {
|
if (response.statusCode === 200) {
|
||||||
loadingRef.current = 1;
|
setStates({
|
||||||
|
loading: false,
|
||||||
if (response.statusCode === 200) {
|
error: false,
|
||||||
setStates({
|
success: true,
|
||||||
loading: false,
|
payload: response.payload,
|
||||||
error: false,
|
errorMessage: '',
|
||||||
success: true,
|
});
|
||||||
payload: response.payload,
|
} else {
|
||||||
errorMessage: '',
|
setStates({
|
||||||
});
|
loading: false,
|
||||||
} else {
|
error: true,
|
||||||
setStates({
|
success: false,
|
||||||
loading: false,
|
payload: undefined,
|
||||||
error: true,
|
errorMessage: response.error as string,
|
||||||
success: false,
|
});
|
||||||
payload: undefined,
|
}
|
||||||
errorMessage: response.error as string,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStates({
|
setStates({
|
||||||
payload: undefined,
|
payload: undefined,
|
||||||
@ -59,13 +50,16 @@ function useFetch<PayloadProps, FunctionParams>(
|
|||||||
errorMessage: error as string,
|
errorMessage: error as string,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (): void => {
|
}, [functions, param]);
|
||||||
loadingRef.current = 1;
|
|
||||||
};
|
// Initial fetch
|
||||||
}, [functions, param, state.loading]);
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
refetch: fetchData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user