mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-16 04:25:54 +08:00
feat: feature flag is updated (#2666)
* feat: flag is updated * feat: feature flag is updated * feat: onrefetch is added on several actions on app * chore: tab is updated * chore: creating dashbaord error is handled * fix: message is fixed * chore: jest test is updated
This commit is contained in:
parent
e7f5adc8a9
commit
604d98be05
@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/features/getFeatures';
|
||||
|
||||
const getFeaturesFlags = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get(`/featureFlags`);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getFeaturesFlags;
|
@ -1,7 +0,0 @@
|
||||
// keep this consistent with backend model>features.go
|
||||
export enum FeatureKeys {
|
||||
SSO = 'SSO',
|
||||
ENTERPRISE_PLAN = 'ENTERPRISE_PLAN',
|
||||
BASIC_PLAN = 'BASIC_PLAN',
|
||||
DISABLE_UPSELL = 'DISABLE_UPSELL',
|
||||
}
|
@ -1,6 +1,11 @@
|
||||
// keep this consistent with backend constants.go
|
||||
export enum FeatureKeys {
|
||||
SSO = 'SSO',
|
||||
ENTERPRISE_PLAN = 'ENTERPRISE_PLAN',
|
||||
BASIC_PLAN = 'BASIC_PLAN',
|
||||
DurationSort = 'DurationSort',
|
||||
TimestampSort = 'TimestampSort',
|
||||
SMART_TRACE_DETAIL = 'SMART_TRACE_DETAIL',
|
||||
CUSTOM_METRICS_FUNCTION = 'CUSTOM_METRICS_FUNCTION',
|
||||
QUERY_BUILDER_PANELS = 'QUERY_BUILDER_PANELS',
|
||||
QUERY_BUILDER_ALERTS = 'QUERY_BUILDER_ALERTS',
|
||||
DISABLE_UPSELL = 'DISABLE_UPSELL',
|
||||
}
|
||||
|
3
frontend/src/constants/reactQueryKeys.ts
Normal file
3
frontend/src/constants/reactQueryKeys.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const REACT_QUERY_KEY = {
|
||||
GET_ALL_LICENCES: 'GET_ALL_LICENCES',
|
||||
};
|
@ -18,7 +18,7 @@ import {
|
||||
UPDATE_CONFIGS,
|
||||
UPDATE_CURRENT_ERROR,
|
||||
UPDATE_CURRENT_VERSION,
|
||||
UPDATE_FEATURE_FLAGS,
|
||||
UPDATE_FEATURE_FLAG_RESPONSE,
|
||||
UPDATE_LATEST_VERSION,
|
||||
UPDATE_LATEST_VERSION_ERROR,
|
||||
} from 'types/actions/app';
|
||||
@ -129,19 +129,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
message: t('oops_something_went_wrong_version'),
|
||||
});
|
||||
}
|
||||
if (
|
||||
getFeaturesResponse.isFetched &&
|
||||
getFeaturesResponse.isSuccess &&
|
||||
getFeaturesResponse.data &&
|
||||
getFeaturesResponse.data.payload
|
||||
) {
|
||||
dispatch({
|
||||
type: UPDATE_FEATURE_FLAGS,
|
||||
payload: {
|
||||
...getFeaturesResponse.data.payload,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
getUserVersionResponse.isFetched &&
|
||||
@ -173,20 +160,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
getFeaturesResponse.isFetched &&
|
||||
getFeaturesResponse.isSuccess &&
|
||||
getFeaturesResponse.data &&
|
||||
getFeaturesResponse.data.payload
|
||||
) {
|
||||
dispatch({
|
||||
type: UPDATE_FEATURE_FLAGS,
|
||||
payload: {
|
||||
...getFeaturesResponse.data.payload,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
getDynamicConfigsResponse.isFetched &&
|
||||
getDynamicConfigsResponse.isSuccess &&
|
||||
@ -226,6 +199,29 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
notifications,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
getFeaturesResponse.isFetched &&
|
||||
getFeaturesResponse.isSuccess &&
|
||||
getFeaturesResponse.data &&
|
||||
getFeaturesResponse.data.payload
|
||||
) {
|
||||
dispatch({
|
||||
type: UPDATE_FEATURE_FLAG_RESPONSE,
|
||||
payload: {
|
||||
featureFlag: getFeaturesResponse.data.payload,
|
||||
refetch: getFeaturesResponse.refetch,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
getFeaturesResponse.data,
|
||||
getFeaturesResponse.isFetched,
|
||||
getFeaturesResponse.isSuccess,
|
||||
getFeaturesResponse.refetch,
|
||||
]);
|
||||
|
||||
const isToDisplayLayout = isLoggedIn;
|
||||
|
||||
return (
|
||||
|
@ -36,8 +36,16 @@ export const alertDefaults: AlertDef = {
|
||||
},
|
||||
},
|
||||
promQueries: {},
|
||||
chQueries: {},
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
chQueries: {
|
||||
A: {
|
||||
name: 'A',
|
||||
query: ``,
|
||||
rawQuery: ``,
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
op: defaultCompareOp,
|
||||
|
@ -2,7 +2,7 @@ import { Button, Tabs } from 'antd';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { IChQueries, IPromQueries } from 'types/api/alerts/compositeQuery';
|
||||
@ -91,11 +91,14 @@ function QuerySection({
|
||||
},
|
||||
];
|
||||
|
||||
const items = [
|
||||
{ label: t('tab_qb'), key: EQueryType.QUERY_BUILDER },
|
||||
{ label: t('tab_chquery'), key: EQueryType.CLICKHOUSE },
|
||||
{ label: t('tab_promql'), key: EQueryType.PROM },
|
||||
];
|
||||
const items = useMemo(
|
||||
() => [
|
||||
{ label: t('tab_qb'), key: EQueryType.QUERY_BUILDER },
|
||||
{ label: t('tab_chquery'), key: EQueryType.CLICKHOUSE },
|
||||
{ label: t('tab_promql'), key: EQueryType.PROM },
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const renderTabs = (typ: AlertTypes): JSX.Element | null => {
|
||||
switch (typ) {
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { Col, FormInstance, Modal, Typography } from 'antd';
|
||||
import { Col, FormInstance, Modal, Tooltip, Typography } from 'antd';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import testAlertApi from 'api/alerts/testAlert';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
|
||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
@ -145,6 +147,7 @@ function FormAlertRules({
|
||||
// onQueryCategoryChange handles changes to query category
|
||||
// in state as well as sets additional defaults
|
||||
const onQueryCategoryChange = (val: EQueryType): void => {
|
||||
console.log('onQueryCategoryChange', val);
|
||||
setQueryCategory(val);
|
||||
if (val === EQueryType.PROM) {
|
||||
setAlertDef({
|
||||
@ -298,6 +301,10 @@ function FormAlertRules({
|
||||
initQuery,
|
||||
]);
|
||||
|
||||
const isAlertAvialable = useIsFeatureDisabled(
|
||||
FeatureKeys.QUERY_BUILDER_ALERTS,
|
||||
);
|
||||
|
||||
const saveRule = useCallback(async () => {
|
||||
if (!isFormValid()) {
|
||||
return;
|
||||
@ -437,6 +444,12 @@ function FormAlertRules({
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
/>
|
||||
);
|
||||
|
||||
const isNewRule = ruleId === 0;
|
||||
|
||||
const isAlertAvialableToSave =
|
||||
isAlertAvialable && isNewRule && queryCategory === EQueryType.QUERY_BUILDER;
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
@ -469,14 +482,18 @@ function FormAlertRules({
|
||||
|
||||
{renderBasicInfo()}
|
||||
<ButtonContainer>
|
||||
<ActionButton
|
||||
loading={loading || false}
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
>
|
||||
{ruleId > 0 ? t('button_savechanges') : t('button_createrule')}
|
||||
</ActionButton>
|
||||
<Tooltip title={isAlertAvialableToSave ? MESSAGE.ALERT : ''}>
|
||||
<ActionButton
|
||||
loading={loading || false}
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
disabled={isAlertAvialableToSave}
|
||||
>
|
||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
|
||||
<ActionButton
|
||||
loading={loading || false}
|
||||
type="default"
|
||||
|
@ -2,12 +2,14 @@ import { Typography } from 'antd';
|
||||
import { ChartData } from 'chart.js';
|
||||
import Spinner from 'components/Spinner';
|
||||
import GridGraphComponent from 'container/GridGraphComponent';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import usePreviousValue from 'hooks/usePreviousValue';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useQuery } from 'react-query';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
@ -20,8 +22,8 @@ import {
|
||||
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { GlobalTime } from 'types/actions/globalTime';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
@ -46,18 +48,22 @@ function GridCardGraph({
|
||||
initialInView: true,
|
||||
});
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { t } = useTranslation(['common']);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [modal, setModal] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalTime>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
@ -122,9 +128,27 @@ function GridCardGraph({
|
||||
|
||||
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
|
||||
|
||||
deleteWidget({ widgetId, setLayout });
|
||||
onToggleModal(setDeleteModal);
|
||||
}, [deleteWidget, layout, onToggleModal, setLayout, widget]);
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(() => {
|
||||
deleteWidget({ widgetId, setLayout });
|
||||
onToggleModal(setDeleteModal);
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.error({
|
||||
message: t('common:something_went_wrong'),
|
||||
});
|
||||
});
|
||||
}, [
|
||||
widget,
|
||||
layout,
|
||||
featureResponse,
|
||||
deleteWidget,
|
||||
setLayout,
|
||||
onToggleModal,
|
||||
notifications,
|
||||
t,
|
||||
]);
|
||||
|
||||
const getModals = (): JSX.Element => (
|
||||
<>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
|
||||
import { Typography } from 'antd';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useFeatureFlag, { MESSAGE } from 'hooks/useFeatureFlag';
|
||||
import React from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useSelector } from 'react-redux';
|
||||
@ -14,6 +17,7 @@ import {
|
||||
ButtonContainer,
|
||||
Card,
|
||||
CardContainer,
|
||||
NoPanelAvialable,
|
||||
ReactGridLayout,
|
||||
} from './styles';
|
||||
|
||||
@ -35,6 +39,8 @@ function GraphLayout({
|
||||
role,
|
||||
);
|
||||
|
||||
const queryBuilderFeature = useFeatureFlag(FeatureKeys.QUERY_BUILDER_PANELS);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonContainer>
|
||||
@ -74,9 +80,31 @@ function GraphLayout({
|
||||
onLayoutChange={onLayoutChangeHandler}
|
||||
draggableHandle=".drag-handle"
|
||||
>
|
||||
{layouts.map(({ Component, ...rest }) => {
|
||||
{layouts.map(({ Component, ...rest }, layoutIndex) => {
|
||||
const currentWidget = (widgets || [])?.find((e) => e.id === rest.i);
|
||||
|
||||
const usageLimit = queryBuilderFeature?.usage_limit || 0;
|
||||
|
||||
const isPanelNotAvialable = usageLimit > 0 && usageLimit <= layoutIndex;
|
||||
|
||||
if (isPanelNotAvialable) {
|
||||
return (
|
||||
<CardContainer
|
||||
data-grid={rest}
|
||||
isDarkMode={isDarkMode}
|
||||
key={currentWidget?.id}
|
||||
>
|
||||
<Card>
|
||||
<Typography.Text type="danger">
|
||||
<NoPanelAvialable isDarkMode={isDarkMode}>
|
||||
{MESSAGE.WIDGET.replace('{{widget}}', usageLimit.toString())}
|
||||
</NoPanelAvialable>
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
isDarkMode={isDarkMode}
|
||||
|
@ -84,6 +84,8 @@ function GridGraph(props: Props): JSX.Element {
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
(async (): Promise<void> => {
|
||||
if (!isAddWidget) {
|
||||
@ -119,6 +121,12 @@ function GridGraph(props: Props): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const errorMessage = t('common:something_went_wrong');
|
||||
|
||||
const onLayoutSaveHandler = useCallback(
|
||||
async (layout: Layout[]) => {
|
||||
try {
|
||||
@ -128,44 +136,62 @@ function GridGraph(props: Props): JSX.Element {
|
||||
errorMessage: '',
|
||||
loading: true,
|
||||
}));
|
||||
const updatedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
name: data.name,
|
||||
tags: data.tags,
|
||||
widgets: data.widgets,
|
||||
variables: data.variables,
|
||||
layout,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
// Save layout only when users has the has the permission to do so.
|
||||
if (saveLayoutPermission) {
|
||||
const response = await updateDashboardApi(updatedDashboard);
|
||||
if (response.statusCode === 200) {
|
||||
setSaveLayoutState((state) => ({
|
||||
...state,
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
}));
|
||||
dispatch({
|
||||
type: UPDATE_DASHBOARD,
|
||||
payload: updatedDashboard,
|
||||
});
|
||||
} else {
|
||||
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(async () => {
|
||||
const updatedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
name: data.name,
|
||||
tags: data.tags,
|
||||
widgets: data.widgets,
|
||||
variables: data.variables,
|
||||
layout,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
// Save layout only when users has the has the permission to do so.
|
||||
if (saveLayoutPermission) {
|
||||
const response = await updateDashboardApi(updatedDashboard);
|
||||
if (response.statusCode === 200) {
|
||||
setSaveLayoutState((state) => ({
|
||||
...state,
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
}));
|
||||
dispatch({
|
||||
type: UPDATE_DASHBOARD,
|
||||
payload: updatedDashboard,
|
||||
});
|
||||
} else {
|
||||
setSaveLayoutState((state) => ({
|
||||
...state,
|
||||
error: true,
|
||||
errorMessage: response.error || errorMessage,
|
||||
loading: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setSaveLayoutState((state) => ({
|
||||
...state,
|
||||
error: true,
|
||||
errorMessage: response.error || 'Something went wrong',
|
||||
errorMessage,
|
||||
loading: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
notifications.error({
|
||||
message: errorMessage,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notifications.error({
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
@ -176,6 +202,9 @@ function GridGraph(props: Props): JSX.Element {
|
||||
data.variables,
|
||||
data.widgets,
|
||||
dispatch,
|
||||
errorMessage,
|
||||
featureResponse,
|
||||
notifications,
|
||||
saveLayoutPermission,
|
||||
selectedDashboard,
|
||||
],
|
||||
@ -207,8 +236,6 @@ function GridGraph(props: Props): JSX.Element {
|
||||
[widgets, onDragSelect],
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(async () => {
|
||||
try {
|
||||
const id = 'empty';
|
||||
@ -239,10 +266,10 @@ function GridGraph(props: Props): JSX.Element {
|
||||
setLayoutFunction(layout);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: error instanceof Error ? error.toString() : 'Something went wrong',
|
||||
message: error instanceof Error ? error.toString() : errorMessage,
|
||||
});
|
||||
}
|
||||
}, [data, selectedDashboard, setLayoutFunction, notifications]);
|
||||
}, [data, selectedDashboard, setLayoutFunction, notifications, errorMessage]);
|
||||
|
||||
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
|
||||
setLayoutFunction(layout);
|
||||
@ -253,43 +280,57 @@ function GridGraph(props: Props): JSX.Element {
|
||||
const onAddPanelHandler = useCallback(() => {
|
||||
try {
|
||||
setAddPanelLoading(true);
|
||||
const isEmptyLayoutPresent =
|
||||
layouts.find((e) => e.i === 'empty') !== undefined;
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(() => {
|
||||
const isEmptyLayoutPresent =
|
||||
layouts.find((e) => e.i === 'empty') !== undefined;
|
||||
|
||||
if (!isEmptyLayoutPresent) {
|
||||
onEmptyWidgetHandler()
|
||||
.then(() => {
|
||||
setAddPanelLoading(false);
|
||||
if (!isEmptyLayoutPresent) {
|
||||
onEmptyWidgetHandler()
|
||||
.then(() => {
|
||||
setAddPanelLoading(false);
|
||||
toggleAddWidget(true);
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.error({
|
||||
message: errorMessage,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
toggleAddWidget(true);
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.error(t('something_went_wrong'));
|
||||
});
|
||||
} else {
|
||||
toggleAddWidget(true);
|
||||
setAddPanelLoading(false);
|
||||
}
|
||||
setAddPanelLoading(false);
|
||||
}
|
||||
})
|
||||
.catch(() =>
|
||||
notifications.error({
|
||||
message: errorMessage,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
if (typeof error === 'string') {
|
||||
notifications.error({
|
||||
message: error || t('something_went_wrong'),
|
||||
});
|
||||
}
|
||||
notifications.error({
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
}, [layouts, onEmptyWidgetHandler, t, toggleAddWidget, notifications]);
|
||||
}, [
|
||||
featureResponse,
|
||||
layouts,
|
||||
onEmptyWidgetHandler,
|
||||
toggleAddWidget,
|
||||
notifications,
|
||||
errorMessage,
|
||||
]);
|
||||
|
||||
return (
|
||||
<GraphLayoutContainer
|
||||
{...{
|
||||
addPanelLoading,
|
||||
layouts,
|
||||
onAddPanelHandler,
|
||||
onLayoutChangeHandler,
|
||||
onLayoutSaveHandler,
|
||||
saveLayoutState,
|
||||
widgets,
|
||||
setLayout,
|
||||
}}
|
||||
addPanelLoading={addPanelLoading}
|
||||
layouts={layouts}
|
||||
onAddPanelHandler={onAddPanelHandler}
|
||||
onLayoutChangeHandler={onLayoutChangeHandler}
|
||||
onLayoutSaveHandler={onLayoutSaveHandler}
|
||||
saveLayoutState={saveLayoutState}
|
||||
setLayout={setLayout}
|
||||
widgets={widgets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -79,3 +79,11 @@ export const Button = styled(ButtonComponent)`
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const NoPanelAvialable = styled.div<Props>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
height: 100%;
|
||||
`;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Typography } from 'antd';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { Spin, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
||||
import history from 'lib/history';
|
||||
import React from 'react';
|
||||
|
||||
@ -12,7 +11,22 @@ import {
|
||||
} from './styles';
|
||||
|
||||
function ManageLicense({ onToggle }: ManageLicenseProps): JSX.Element {
|
||||
const isEnterprise = useFeatureFlags(FeatureKeys.ENTERPRISE_PLAN);
|
||||
const { data, isLoading } = useLicense();
|
||||
|
||||
const onManageLicense = (): void => {
|
||||
onToggle();
|
||||
history.push(ROUTES.LIST_LICENSES);
|
||||
};
|
||||
|
||||
if (isLoading || data?.payload === undefined) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
const isEnterprise = data?.payload?.some(
|
||||
(license) =>
|
||||
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.ENTERPRISE_PLAN,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography>SIGNOZ STATUS</Typography>
|
||||
@ -23,14 +37,7 @@ function ManageLicense({ onToggle }: ManageLicenseProps): JSX.Element {
|
||||
<Typography>{!isEnterprise ? 'Free Plan' : 'Enterprise Plan'} </Typography>
|
||||
</ManageLicenseWrapper>
|
||||
|
||||
<Typography.Link
|
||||
onClick={(): void => {
|
||||
onToggle();
|
||||
history.push(ROUTES.LIST_LICENSES);
|
||||
}}
|
||||
>
|
||||
Manage Licenses
|
||||
</Typography.Link>
|
||||
<Typography.Link onClick={onManageLicense}>Manage Licenses</Typography.Link>
|
||||
</ManageLicenseContainer>
|
||||
</>
|
||||
);
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { Button, Form, Input } from 'antd';
|
||||
import getFeaturesFlags from 'api/features/getFeatureFlags';
|
||||
import apply from 'api/licenses/apply';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { QueryObserverResult, RefetchOptions, useQuery } from 'react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppAction, UPDATE_FEATURE_FLAGS } from 'types/actions/app';
|
||||
import { QueryObserverResult, RefetchOptions } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/licenses/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import { ApplyForm, ApplyFormContainer, LicenseInput } from './styles';
|
||||
|
||||
@ -21,12 +20,9 @@ function ApplyLicenseForm({
|
||||
const { t } = useTranslation(['licenses']);
|
||||
const [key, setKey] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const dispatch = useDispatch<Dispatch<AppAction>>();
|
||||
const { refetch } = useQuery({
|
||||
queryFn: getFeaturesFlags,
|
||||
queryKey: 'getFeatureFlags',
|
||||
enabled: false,
|
||||
});
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
@ -47,16 +43,8 @@ function ApplyLicenseForm({
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
const [featureFlagsResponse] = await Promise.all([
|
||||
refetch(),
|
||||
licenseRefetch(),
|
||||
]);
|
||||
if (featureFlagsResponse.data?.payload) {
|
||||
dispatch({
|
||||
type: UPDATE_FEATURE_FLAGS,
|
||||
payload: featureFlagsResponse.data.payload,
|
||||
});
|
||||
}
|
||||
await Promise.all([featureResponse?.refetch(), licenseRefetch()]);
|
||||
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: t('license_applied'),
|
||||
|
@ -1,19 +1,15 @@
|
||||
import { Tabs, Typography } from 'antd';
|
||||
import getAll from 'api/licenses/getAll';
|
||||
import Spinner from 'components/Spinner';
|
||||
import useLicense from 'hooks/useLicense';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import ApplyLicenseForm from './ApplyLicenseForm';
|
||||
import ListLicenses from './ListLicenses';
|
||||
|
||||
function Licenses(): JSX.Element {
|
||||
const { t } = useTranslation(['licenses']);
|
||||
const { data, isError, isLoading, refetch } = useQuery({
|
||||
queryFn: getAll,
|
||||
queryKey: 'getAllLicenses',
|
||||
});
|
||||
const { data, isError, isLoading, refetch } = useLicense();
|
||||
|
||||
if (isError || data?.error) {
|
||||
return <Typography>{data?.error}</Typography>;
|
||||
|
@ -2,8 +2,11 @@ import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import deleteAlerts from 'api/alerts/delete';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { PayloadProps as DeleteAlertPayloadProps } from 'types/api/alerts/delete';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import { ColumnButton } from './styles';
|
||||
|
||||
@ -22,15 +25,14 @@ function DeleteAlert({
|
||||
payload: undefined,
|
||||
});
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const defaultErrorMessage = 'Something went wrong';
|
||||
|
||||
const onDeleteHandler = async (id: number): Promise<void> => {
|
||||
try {
|
||||
setDeleteAlertState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
|
||||
const response = await deleteAlerts({
|
||||
id,
|
||||
});
|
||||
@ -72,11 +74,32 @@ function DeleteAlert({
|
||||
}
|
||||
};
|
||||
|
||||
const onClickHandler = (): void => {
|
||||
setDeleteAlertState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(() => {
|
||||
onDeleteHandler(id);
|
||||
})
|
||||
.catch(() => {
|
||||
setDeleteAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
}));
|
||||
notifications.error({
|
||||
message: defaultErrorMessage,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ColumnButton
|
||||
disabled={deleteAlertState.loading || false}
|
||||
loading={deleteAlertState.loading || false}
|
||||
onClick={(): Promise<void> => onDeleteHandler(id)}
|
||||
onClick={onClickHandler}
|
||||
type="link"
|
||||
>
|
||||
Delete
|
||||
|
@ -26,7 +26,9 @@ import ToggleAlertState from './ToggleAlertState';
|
||||
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
const [data, setData] = useState<GettableAlert[]>(allAlertRules || []);
|
||||
const { t } = useTranslation('common');
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const { role, featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const [addNewAlert, action] = useComponentPermission(
|
||||
['add_new_alert', 'action'],
|
||||
role,
|
||||
@ -48,12 +50,28 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
})();
|
||||
}, 30000);
|
||||
|
||||
const handleError = useCallback((): void => {
|
||||
notificationsApi.error({
|
||||
message: t('something_went_wrong'),
|
||||
});
|
||||
}, [notificationsApi, t]);
|
||||
|
||||
const onClickNewAlertHandler = useCallback(() => {
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
}, []);
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(() => {
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
})
|
||||
.catch(handleError);
|
||||
}, [featureResponse, handleError]);
|
||||
|
||||
const onEditHandler = (id: string): void => {
|
||||
history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${id}`);
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(() => {
|
||||
history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${id}`);
|
||||
})
|
||||
.catch(handleError);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<GettableAlert> = [
|
||||
|
@ -4,6 +4,7 @@ import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import Editor from 'components/Editor';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { MESSAGE } from 'hooks/useFeatureFlag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import React, { useState } from 'react';
|
||||
@ -28,6 +29,8 @@ function ImportJSON({
|
||||
const [isCreateDashboardError, setIsCreateDashboardError] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const [isFeatureAlert, setIsFeatureAlert] = useState<boolean>(false);
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const [dashboardCreating, setDashboardCreating] = useState<boolean>(false);
|
||||
@ -99,6 +102,15 @@ function ImportJSON({
|
||||
}),
|
||||
);
|
||||
}, 10);
|
||||
} else if (response.error === 'feature usage exceeded') {
|
||||
setIsFeatureAlert(true);
|
||||
notifications.error({
|
||||
message:
|
||||
response.error ||
|
||||
t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
setIsCreateDashboardError(true);
|
||||
notifications.error({
|
||||
@ -112,6 +124,7 @@ function ImportJSON({
|
||||
setDashboardCreating(false);
|
||||
} catch {
|
||||
setDashboardCreating(false);
|
||||
setIsFeatureAlert(false);
|
||||
|
||||
setIsCreateDashboardError(true);
|
||||
}
|
||||
@ -124,6 +137,13 @@ function ImportJSON({
|
||||
</Space>
|
||||
);
|
||||
|
||||
const onCancelHandler = (): void => {
|
||||
setIsUploadJSONError(false);
|
||||
setIsCreateDashboardError(false);
|
||||
setIsFeatureAlert(false);
|
||||
onModalHandler();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isImportJSONModalVisible}
|
||||
@ -131,7 +151,7 @@ function ImportJSON({
|
||||
maskClosable
|
||||
destroyOnClose
|
||||
width="70vw"
|
||||
onCancel={onModalHandler}
|
||||
onCancel={onCancelHandler}
|
||||
title={
|
||||
<>
|
||||
<Typography.Title level={4}>{t('import_json')}</Typography.Title>
|
||||
@ -148,6 +168,11 @@ function ImportJSON({
|
||||
{t('load_json')}
|
||||
</Button>
|
||||
{isCreateDashboardError && getErrorNode(t('error_loading_json'))}
|
||||
{isFeatureAlert && (
|
||||
<Typography.Text type="danger">
|
||||
{MESSAGE.CREATE_DASHBOARD}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</FooterContainer>
|
||||
}
|
||||
>
|
||||
|
@ -74,49 +74,52 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
const columns: TableColumnProps<Data>[] = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
width: 100,
|
||||
render: Name,
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
width: 100,
|
||||
dataIndex: 'description',
|
||||
},
|
||||
{
|
||||
title: 'Tags (can be multiple)',
|
||||
dataIndex: 'tags',
|
||||
width: 80,
|
||||
render: Tags,
|
||||
},
|
||||
{
|
||||
title: 'Created At',
|
||||
dataIndex: 'createdBy',
|
||||
width: 80,
|
||||
sorter: (a: Data, b: Data): number => {
|
||||
const prev = new Date(a.createdBy).getTime();
|
||||
const next = new Date(b.createdBy).getTime();
|
||||
|
||||
return prev - next;
|
||||
const columns: TableColumnProps<Data>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
width: 100,
|
||||
render: Name,
|
||||
},
|
||||
render: Createdby,
|
||||
},
|
||||
{
|
||||
title: 'Last Updated Time',
|
||||
width: 90,
|
||||
dataIndex: 'lastUpdatedTime',
|
||||
sorter: (a: Data, b: Data): number => {
|
||||
const prev = new Date(a.lastUpdatedTime).getTime();
|
||||
const next = new Date(b.lastUpdatedTime).getTime();
|
||||
|
||||
return prev - next;
|
||||
{
|
||||
title: 'Description',
|
||||
width: 100,
|
||||
dataIndex: 'description',
|
||||
},
|
||||
render: DateComponent,
|
||||
},
|
||||
];
|
||||
{
|
||||
title: 'Tags (can be multiple)',
|
||||
dataIndex: 'tags',
|
||||
width: 80,
|
||||
render: Tags,
|
||||
},
|
||||
{
|
||||
title: 'Created At',
|
||||
dataIndex: 'createdBy',
|
||||
width: 80,
|
||||
sorter: (a: Data, b: Data): number => {
|
||||
const prev = new Date(a.createdBy).getTime();
|
||||
const next = new Date(b.createdBy).getTime();
|
||||
|
||||
return prev - next;
|
||||
},
|
||||
render: Createdby,
|
||||
},
|
||||
{
|
||||
title: 'Last Updated Time',
|
||||
width: 90,
|
||||
dataIndex: 'lastUpdatedTime',
|
||||
sorter: (a: Data, b: Data): number => {
|
||||
const prev = new Date(a.lastUpdatedTime).getTime();
|
||||
const next = new Date(b.lastUpdatedTime).getTime();
|
||||
|
||||
return prev - next;
|
||||
},
|
||||
render: DateComponent,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
if (action) {
|
||||
columns.push({
|
||||
@ -199,7 +202,7 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
setUploadedGrafana(uploadedGrafana);
|
||||
};
|
||||
|
||||
const getMenuItems = useCallback(() => {
|
||||
const getMenuItems = useMemo(() => {
|
||||
const menuItems: ItemType[] = [];
|
||||
if (createNewDashboard) {
|
||||
menuItems.push({
|
||||
@ -227,7 +230,7 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
|
||||
const menu: MenuProps = useMemo(
|
||||
() => ({
|
||||
items: getMenuItems(),
|
||||
items: getMenuItems,
|
||||
}),
|
||||
[getMenuItems],
|
||||
);
|
||||
@ -245,7 +248,7 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
}}
|
||||
/>
|
||||
{newDashboard && (
|
||||
<Dropdown trigger={['click']} menu={menu}>
|
||||
<Dropdown disabled={loading} trigger={['click']} menu={menu}>
|
||||
<NewDashboardButton
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
@ -260,11 +263,12 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
</Row>
|
||||
),
|
||||
[
|
||||
getText,
|
||||
newDashboard,
|
||||
newDashboardState.error,
|
||||
newDashboardState.loading,
|
||||
loading,
|
||||
menu,
|
||||
newDashboardState.loading,
|
||||
newDashboardState.error,
|
||||
getText,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -34,10 +34,12 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
|
||||
CLICKHOUSE: uuid(),
|
||||
PROM: uuid(),
|
||||
});
|
||||
|
||||
const { dashboards, isLoadingQueryResult } = useSelector<
|
||||
AppState,
|
||||
DashboardReducer
|
||||
>((state) => state.dashboards);
|
||||
|
||||
const [selectedDashboards] = dashboards;
|
||||
const { search } = useLocation();
|
||||
const { widgets } = selectedDashboards.data;
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { Button, Modal, Typography } from 'antd';
|
||||
import { LockFilled } from '@ant-design/icons';
|
||||
import { Button, Modal, Tooltip, Typography } from 'antd';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
|
||||
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import history from 'lib/history';
|
||||
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
||||
@ -22,6 +26,7 @@ import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { FLUSH_DASHBOARD } from 'types/actions/dashboard';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
@ -51,6 +56,10 @@ function NewWidget({
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const [selectedDashboard] = dashboards;
|
||||
|
||||
const { widgets } = selectedDashboard.data;
|
||||
@ -99,22 +108,34 @@ function NewWidget({
|
||||
enum: selectedWidget?.timePreferance || 'GLOBAL_TIME',
|
||||
});
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onClickSaveHandler = useCallback(() => {
|
||||
// update the global state
|
||||
saveSettingOfPanel({
|
||||
uuid: selectedDashboard.uuid,
|
||||
description,
|
||||
isStacked: stacked,
|
||||
nullZeroValues: selectedNullZeroValue,
|
||||
opacity,
|
||||
timePreferance: selectedTime.enum,
|
||||
title,
|
||||
yAxisUnit,
|
||||
widgetId: query.get('widgetId') || '',
|
||||
dashboardId,
|
||||
graphType,
|
||||
});
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(() => {
|
||||
saveSettingOfPanel({
|
||||
uuid: selectedDashboard.uuid,
|
||||
description,
|
||||
isStacked: stacked,
|
||||
nullZeroValues: selectedNullZeroValue,
|
||||
opacity,
|
||||
timePreferance: selectedTime.enum,
|
||||
title,
|
||||
yAxisUnit,
|
||||
widgetId: query.get('widgetId') || '',
|
||||
dashboardId,
|
||||
graphType,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.error({
|
||||
message: 'Something went wrong',
|
||||
});
|
||||
});
|
||||
}, [
|
||||
featureResponse,
|
||||
saveSettingOfPanel,
|
||||
selectedDashboard.uuid,
|
||||
description,
|
||||
@ -127,6 +148,7 @@ function NewWidget({
|
||||
query,
|
||||
dashboardId,
|
||||
graphType,
|
||||
notifications,
|
||||
]);
|
||||
|
||||
const onClickDiscardHandler = useCallback(() => {
|
||||
@ -167,13 +189,39 @@ function NewWidget({
|
||||
getQueryResult();
|
||||
}, [getQueryResult]);
|
||||
|
||||
const onSaveDashboard = useCallback((): void => {
|
||||
setSaveModal(true);
|
||||
}, []);
|
||||
|
||||
const isQueryBuilderActive = useIsFeatureDisabled(
|
||||
FeatureKeys.QUERY_BUILDER_PANELS,
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ButtonContainer>
|
||||
<Button type="primary" onClick={(): void => setSaveModal(true)}>
|
||||
Save
|
||||
</Button>
|
||||
{/* <Button onClick={onClickApplyHandler}>Apply</Button> */}
|
||||
{isQueryBuilderActive && (
|
||||
<Tooltip title={MESSAGE.PANEL}>
|
||||
<Button
|
||||
icon={<LockFilled />}
|
||||
type="primary"
|
||||
disabled={isQueryBuilderActive}
|
||||
onClick={onSaveDashboard}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isQueryBuilderActive && (
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={isQueryBuilderActive}
|
||||
onClick={onSaveDashboard}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={onClickDiscardHandler}>Discard</Button>
|
||||
</ButtonContainer>
|
||||
|
||||
|
@ -3,8 +3,8 @@ import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Input, Modal, Typography } from 'antd';
|
||||
import { useForm } from 'antd/es/form/Form';
|
||||
import createDomainApi from 'api/SAML/postDomain';
|
||||
import { FeatureKeys } from 'constants/featureKeys';
|
||||
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import useFeatureFlag from 'hooks/useFeatureFlag/useFeatureFlag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -18,7 +18,7 @@ function AddDomain({ refetch }: Props): JSX.Element {
|
||||
const { t } = useTranslation(['common', 'organizationsettings']);
|
||||
const [isAddDomains, setIsDomain] = useState(false);
|
||||
const [form] = useForm<FormProps>();
|
||||
const SSOFlag = useFeatureFlag(FeatureKeys.SSO);
|
||||
const isSsoFlagEnabled = useFeatureFlag(FeatureKeys.SSO);
|
||||
|
||||
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
@ -58,7 +58,7 @@ function AddDomain({ refetch }: Props): JSX.Element {
|
||||
ns: 'organizationsettings',
|
||||
})}
|
||||
</Typography.Title>
|
||||
{SSOFlag && (
|
||||
{isSsoFlagEnabled && (
|
||||
<Button
|
||||
onClick={(): void => setIsDomain(true)}
|
||||
type="primary"
|
||||
|
@ -7,8 +7,8 @@ import updateDomain from 'api/SAML/updateDomain';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { SIGNOZ_UPGRADE_PLAN_URL } from 'constants/app';
|
||||
import { FeatureKeys } from 'constants/featureKeys';
|
||||
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import useFeatureFlag from 'hooks/useFeatureFlag/useFeatureFlag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Divider, Space } from 'antd';
|
||||
import { FeatureKeys } from 'constants/featureKeys';
|
||||
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -14,8 +14,11 @@ import PendingInvitesContainer from './PendingInvitesContainer';
|
||||
function OrganizationSettings(): JSX.Element {
|
||||
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const sso = useFeatureFlag(FeatureKeys.SSO);
|
||||
const noUpsell = useFeatureFlag(FeatureKeys.DISABLE_UPSELL);
|
||||
const isNotSSO = useIsFeatureDisabled(FeatureKeys.SSO);
|
||||
|
||||
const isNoUpSell = useIsFeatureDisabled(FeatureKeys.DISABLE_UPSELL);
|
||||
|
||||
const isAuthDomain = !isNoUpSell || (isNoUpSell && !isNotSSO);
|
||||
|
||||
if (!org) {
|
||||
return <div />;
|
||||
@ -38,7 +41,7 @@ function OrganizationSettings(): JSX.Element {
|
||||
<Divider />
|
||||
<Members />
|
||||
<Divider />
|
||||
{(!noUpsell || (noUpsell && sso)) && <AuthDomains />}
|
||||
{isAuthDomain && <AuthDomains />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -16,21 +16,6 @@ import { useSetCurrentKeyAndOperator } from './useSetCurrentKeyAndOperator';
|
||||
import { useTag } from './useTag';
|
||||
import { useTagValidation } from './useTagValidation';
|
||||
|
||||
interface IAutoComplete {
|
||||
updateTag: (value: string) => void;
|
||||
handleSearch: (value: string) => void;
|
||||
handleClearTag: (value: string) => void;
|
||||
handleSelect: (value: string) => void;
|
||||
handleKeyDown: (event: React.KeyboardEvent) => void;
|
||||
options: Option[];
|
||||
tags: string[];
|
||||
searchValue: string;
|
||||
isMulti: boolean;
|
||||
isFetching: boolean;
|
||||
setSearchKey: (value: string) => void;
|
||||
searchKey: string;
|
||||
}
|
||||
|
||||
export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [searchKey, setSearchKey] = useState<string>('');
|
||||
@ -134,3 +119,18 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
|
||||
searchKey,
|
||||
};
|
||||
};
|
||||
|
||||
interface IAutoComplete {
|
||||
updateTag: (value: string) => void;
|
||||
handleSearch: (value: string) => void;
|
||||
handleClearTag: (value: string) => void;
|
||||
handleSelect: (value: string) => void;
|
||||
handleKeyDown: (event: React.KeyboardEvent) => void;
|
||||
options: Option[];
|
||||
tags: string[];
|
||||
searchValue: string;
|
||||
isMulti: boolean;
|
||||
isFetching: boolean;
|
||||
setSearchKey: (value: string) => void;
|
||||
searchKey: string;
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
import _get from 'lodash-es/get';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
const useFeatureFlag = (flagKey: string): boolean => {
|
||||
const { featureFlags } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
return _get(featureFlags, flagKey, false);
|
||||
};
|
||||
|
||||
export default useFeatureFlag;
|
9
frontend/src/hooks/useFeatureFlag/constant.ts
Normal file
9
frontend/src/hooks/useFeatureFlag/constant.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const MESSAGE = {
|
||||
PANEL:
|
||||
'You have exceeded the number of logs and traces based panels using query builder that are allowed in the community edition.',
|
||||
ALERT:
|
||||
'You have exceeded the number of alerts that are allowed in the community edition.',
|
||||
WIDGET: 'You have reached limit of {{widget}} free widgets.',
|
||||
CREATE_DASHBOARD:
|
||||
'You have reached limit of creating the query builder based dashboard panels.',
|
||||
};
|
7
frontend/src/hooks/useFeatureFlag/index.ts
Normal file
7
frontend/src/hooks/useFeatureFlag/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { MESSAGE } from './constant';
|
||||
import useFeatureFlag from './useFeatureFlag';
|
||||
import useIsFeatureDisabled from './useIsFeatureDisabled';
|
||||
|
||||
export default useFeatureFlag;
|
||||
|
||||
export { MESSAGE, useIsFeatureDisabled };
|
29
frontend/src/hooks/useFeatureFlag/useFeatureFlag.ts
Normal file
29
frontend/src/hooks/useFeatureFlag/useFeatureFlag.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
const useFeatureFlag = (
|
||||
flagKey: keyof typeof FeatureKeys,
|
||||
): FlatArray<FeatureFlagPayload, 1> | undefined => {
|
||||
const { featureResponse = [] } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
if (featureResponse === null) return undefined;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const featureResponseData = featureResponse.data as FeatureFlagPayload;
|
||||
|
||||
const feature = featureResponseData?.find((flag) => flag.name === flagKey);
|
||||
|
||||
if (!feature) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return feature;
|
||||
};
|
||||
|
||||
export default useFeatureFlag;
|
11
frontend/src/hooks/useFeatureFlag/useIsFeatureDisabled.ts
Normal file
11
frontend/src/hooks/useFeatureFlag/useIsFeatureDisabled.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
|
||||
import useFeatureFlag from './useFeatureFlag';
|
||||
|
||||
const useIsFeatureDisabled = (props: keyof typeof FeatureKeys): boolean => {
|
||||
const feature = useFeatureFlag(props);
|
||||
|
||||
return !feature?.active ?? false;
|
||||
};
|
||||
|
||||
export default useIsFeatureDisabled;
|
3
frontend/src/hooks/useLicense/constant.ts
Normal file
3
frontend/src/hooks/useLicense/constant.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const LICENSE_PLAN_KEY = {
|
||||
ENTERPRISE_PLAN: 'ENTERPRISE_PLAN',
|
||||
};
|
6
frontend/src/hooks/useLicense/index.ts
Normal file
6
frontend/src/hooks/useLicense/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { LICENSE_PLAN_KEY } from './constant';
|
||||
import useLicense from './useLicense';
|
||||
|
||||
export default useLicense;
|
||||
|
||||
export { LICENSE_PLAN_KEY };
|
25
frontend/src/hooks/useLicense/useLicense.tsx
Normal file
25
frontend/src/hooks/useLicense/useLicense.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import getAll from 'api/licenses/getAll';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/licenses/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
const useLicense = (): UseLicense => {
|
||||
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
return useQuery({
|
||||
queryFn: getAll,
|
||||
queryKey: [REACT_QUERY_KEY.GET_ALL_LICENCES, user?.email],
|
||||
enabled: !!user?.email,
|
||||
});
|
||||
};
|
||||
|
||||
type UseLicense = UseQueryResult<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse,
|
||||
unknown
|
||||
>;
|
||||
|
||||
export default useLicense;
|
@ -9,7 +9,7 @@ import {
|
||||
UPDATE_CONFIGS,
|
||||
UPDATE_CURRENT_ERROR,
|
||||
UPDATE_CURRENT_VERSION,
|
||||
UPDATE_FEATURE_FLAGS,
|
||||
UPDATE_FEATURE_FLAG_RESPONSE,
|
||||
UPDATE_LATEST_VERSION,
|
||||
UPDATE_LATEST_VERSION_ERROR,
|
||||
UPDATE_ORG,
|
||||
@ -47,7 +47,10 @@ const InitialValue: InitialValueTypes = {
|
||||
isSideBarCollapsed: getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
|
||||
currentVersion: '',
|
||||
latestVersion: '',
|
||||
featureFlags: {},
|
||||
featureResponse: {
|
||||
data: null,
|
||||
refetch: Promise.resolve,
|
||||
},
|
||||
isCurrentVersionError: false,
|
||||
isLatestVersionError: false,
|
||||
user: getInitialUser(),
|
||||
@ -80,10 +83,13 @@ const appReducer = (
|
||||
};
|
||||
}
|
||||
|
||||
case UPDATE_FEATURE_FLAGS: {
|
||||
case UPDATE_FEATURE_FLAG_RESPONSE: {
|
||||
return {
|
||||
...state,
|
||||
featureFlags: { ...action.payload },
|
||||
featureResponse: {
|
||||
data: action.payload.featureFlag,
|
||||
refetch: action.payload.refetch,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { QueryObserverBaseResult } from 'react-query';
|
||||
import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags';
|
||||
import {
|
||||
Organization,
|
||||
@ -22,9 +23,9 @@ export const UPDATE_USER_ORG_ROLE = 'UPDATE_USER_ORG_ROLE';
|
||||
export const UPDATE_USER = 'UPDATE_USER';
|
||||
export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME';
|
||||
export const UPDATE_ORG = 'UPDATE_ORG';
|
||||
export const UPDATE_FEATURE_FLAGS = 'UPDATE_FEATURE_FLAGS';
|
||||
export const UPDATE_CONFIGS = 'UPDATE_CONFIGS';
|
||||
export const UPDATE_USER_FLAG = 'UPDATE_USER_FLAG';
|
||||
export const UPDATE_FEATURE_FLAG_RESPONSE = 'UPDATE_FEATURE_FLAG_RESPONSE';
|
||||
|
||||
export interface LoggedInUser {
|
||||
type: typeof LOGGED_IN;
|
||||
@ -38,10 +39,6 @@ export interface SideBarCollapse {
|
||||
payload: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateFeatureFlags {
|
||||
type: typeof UPDATE_FEATURE_FLAGS;
|
||||
payload: null | FeatureFlagPayload;
|
||||
}
|
||||
export interface UpdateAppVersion {
|
||||
type: typeof UPDATE_CURRENT_VERSION;
|
||||
payload: {
|
||||
@ -130,6 +127,14 @@ export interface UpdateConfigs {
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateFeatureFlag {
|
||||
type: typeof UPDATE_FEATURE_FLAG_RESPONSE;
|
||||
payload: {
|
||||
featureFlag: FeatureFlagPayload;
|
||||
refetch: QueryObserverBaseResult['refetch'];
|
||||
};
|
||||
}
|
||||
|
||||
export type AppAction =
|
||||
| LoggedInUser
|
||||
| SideBarCollapse
|
||||
@ -142,6 +147,6 @@ export type AppAction =
|
||||
| UpdateUser
|
||||
| UpdateOrgName
|
||||
| UpdateOrg
|
||||
| UpdateFeatureFlags
|
||||
| UpdateConfigs
|
||||
| UpdateUserFlag;
|
||||
| UpdateUserFlag
|
||||
| UpdateFeatureFlag;
|
||||
|
@ -1,3 +0,0 @@
|
||||
export interface PayloadProps {
|
||||
[key: string]: boolean;
|
||||
}
|
@ -1,3 +1,19 @@
|
||||
export interface PayloadProps {
|
||||
[key: string]: boolean;
|
||||
export type FeaturesFlag =
|
||||
| 'DurationSort'
|
||||
| 'TimestampSort'
|
||||
| 'SMART_TRACE_DETAIL'
|
||||
| 'CUSTOM_METRICS_FUNCTION'
|
||||
| 'QUERY_BUILDER_PANELS'
|
||||
| 'QUERY_BUILDER_ALERTS'
|
||||
| 'DISABLE_UPSELL'
|
||||
| 'SSO';
|
||||
|
||||
interface FeatureFlagProps {
|
||||
name: FeaturesFlag;
|
||||
active: boolean;
|
||||
usage: number;
|
||||
usage_limit: number;
|
||||
route: string;
|
||||
}
|
||||
|
||||
export type PayloadProps = FeatureFlagProps[];
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { QueryObserverBaseResult } from 'react-query';
|
||||
import { PayloadProps as ConfigPayload } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
||||
import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags';
|
||||
import { PayloadProps as OrgPayload } from 'types/api/user/getOrganization';
|
||||
@ -26,9 +27,12 @@ export default interface AppReducer {
|
||||
isUserFetchingError: boolean;
|
||||
role: ROLES | null;
|
||||
org: OrgPayload | null;
|
||||
featureFlags: null | FeatureFlagPayload;
|
||||
configs: ConfigPayload;
|
||||
userFlags: null | UserFlags;
|
||||
ee: 'Y' | 'N';
|
||||
setupCompleted: boolean;
|
||||
featureResponse: {
|
||||
data: FeatureFlagPayload | null;
|
||||
refetch: QueryObserverBaseResult['refetch'];
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user