mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 09:49:02 +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 useComponentPermission from 'hooks/useComponentPermission';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -83,16 +83,22 @@ function BasicInfo({
|
||||
window.open(ROUTES.CHANNELS_NEW, '_blank');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const hasLoggedEvent = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!channels.loading && isNewRule) {
|
||||
if (!channels.loading && isNewRule && !hasLoggedEvent.current) {
|
||||
logEvent('Alert: New alert creation page visited', {
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||
numberOfChannels: channels?.payload?.length,
|
||||
});
|
||||
hasLoggedEvent.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [channels.payload, channels.loading]);
|
||||
}, [channels.loading]);
|
||||
|
||||
const refetchChannels = async (): Promise<void> => {
|
||||
await channels.refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -197,7 +203,7 @@ function BasicInfo({
|
||||
{!shouldBroadCastToAllChannels && (
|
||||
<Tooltip
|
||||
title={
|
||||
noChannels
|
||||
noChannels && !addNewChannelPermission
|
||||
? 'No channels. Ask an admin to create a notification channel'
|
||||
: undefined
|
||||
}
|
||||
@ -212,10 +218,10 @@ function BasicInfo({
|
||||
]}
|
||||
>
|
||||
<ChannelSelect
|
||||
disabled={
|
||||
shouldBroadCastToAllChannels || noChannels || !!channels.loading
|
||||
}
|
||||
onDropdownOpen={refetchChannels}
|
||||
disabled={shouldBroadCastToAllChannels}
|
||||
currentValue={alertDef.preferredChannels}
|
||||
handleCreateNewChannels={handleCreateNewChannels}
|
||||
channels={channels}
|
||||
onSelectChannels={(preferredChannels): void => {
|
||||
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 { useNotifications } from 'hooks/useNotifications';
|
||||
import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
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 {
|
||||
disabled?: boolean;
|
||||
currentValue?: string[];
|
||||
onSelectChannels: (s: string[]) => void;
|
||||
onDropdownOpen: () => void;
|
||||
channels: State<PayloadProps | undefined>;
|
||||
handleCreateNewChannels: () => void;
|
||||
}
|
||||
|
||||
function ChannelSelect({
|
||||
disabled,
|
||||
currentValue,
|
||||
onSelectChannels,
|
||||
onDropdownOpen,
|
||||
channels,
|
||||
handleCreateNewChannels,
|
||||
}: ChannelSelectProps): JSX.Element | null {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
@ -26,6 +35,10 @@ function ChannelSelect({
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const handleChange = (value: string[]): void => {
|
||||
if (value.includes('add-new-channel')) {
|
||||
handleCreateNewChannels();
|
||||
return;
|
||||
}
|
||||
onSelectChannels(value);
|
||||
};
|
||||
|
||||
@ -35,9 +48,27 @@ function ChannelSelect({
|
||||
description: channels.errorMessage,
|
||||
});
|
||||
}
|
||||
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const [addNewChannelPermission] = useComponentPermission(
|
||||
['add_new_channel'],
|
||||
role,
|
||||
);
|
||||
|
||||
const renderOptions = (): 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 (
|
||||
channels.loading ||
|
||||
channels.payload === undefined ||
|
||||
@ -56,6 +87,7 @@ function ChannelSelect({
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledSelect
|
||||
disabled={disabled}
|
||||
@ -65,6 +97,12 @@ function ChannelSelect({
|
||||
placeholder={t('placeholder_channel_select')}
|
||||
data-testid="alert-channel-select"
|
||||
value={currentValue}
|
||||
notFoundContent={channels.loading && <Spin size="small" />}
|
||||
onDropdownVisibleChange={(open): void => {
|
||||
if (open) {
|
||||
onDropdownOpen();
|
||||
}
|
||||
}}
|
||||
onChange={(value): void => {
|
||||
handleChange(value as string[]);
|
||||
}}
|
||||
|
@ -4,3 +4,10 @@ import styled from 'styled-components';
|
||||
export const StyledSelect = styled(Select)`
|
||||
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';
|
||||
|
||||
function useFetch<PayloadProps, FunctionParams>(
|
||||
@ -10,7 +10,7 @@ function useFetch<PayloadProps, FunctionParams>(
|
||||
(arg0: any): Promise<SuccessResponse<PayloadProps> | ErrorResponse>;
|
||||
},
|
||||
param?: FunctionParams,
|
||||
): State<PayloadProps | undefined> {
|
||||
): State<PayloadProps | undefined> & { refetch: () => Promise<void> } {
|
||||
const [state, setStates] = useState<State<PayloadProps | undefined>>({
|
||||
loading: true,
|
||||
success: null,
|
||||
@ -19,37 +19,28 @@ function useFetch<PayloadProps, FunctionParams>(
|
||||
payload: undefined,
|
||||
});
|
||||
|
||||
const loadingRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = useCallback(async (): Promise<void> => {
|
||||
setStates((prev) => ({ ...prev, loading: true }));
|
||||
try {
|
||||
(async (): Promise<void> => {
|
||||
if (state.loading) {
|
||||
const response = await functions(param);
|
||||
const response = await functions(param);
|
||||
|
||||
if (loadingRef.current === 0) {
|
||||
loadingRef.current = 1;
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setStates({
|
||||
loading: false,
|
||||
error: false,
|
||||
success: true,
|
||||
payload: response.payload,
|
||||
errorMessage: '',
|
||||
});
|
||||
} else {
|
||||
setStates({
|
||||
loading: false,
|
||||
error: true,
|
||||
success: false,
|
||||
payload: undefined,
|
||||
errorMessage: response.error as string,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
if (response.statusCode === 200) {
|
||||
setStates({
|
||||
loading: false,
|
||||
error: false,
|
||||
success: true,
|
||||
payload: response.payload,
|
||||
errorMessage: '',
|
||||
});
|
||||
} else {
|
||||
setStates({
|
||||
loading: false,
|
||||
error: true,
|
||||
success: false,
|
||||
payload: undefined,
|
||||
errorMessage: response.error as string,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setStates({
|
||||
payload: undefined,
|
||||
@ -59,13 +50,16 @@ function useFetch<PayloadProps, FunctionParams>(
|
||||
errorMessage: error as string,
|
||||
});
|
||||
}
|
||||
return (): void => {
|
||||
loadingRef.current = 1;
|
||||
};
|
||||
}, [functions, param, state.loading]);
|
||||
}, [functions, param]);
|
||||
|
||||
// Initial fetch
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
refetch: fetchData,
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user