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:
Palash Gupta 2023-05-19 12:19:42 +05:30 committed by GitHub
parent e7f5adc8a9
commit 604d98be05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 645 additions and 323 deletions

View File

@ -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;

View File

@ -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',
}

View File

@ -1,6 +1,11 @@
// keep this consistent with backend constants.go // keep this consistent with backend constants.go
export enum FeatureKeys { export enum FeatureKeys {
SSO = 'SSO', SSO = 'SSO',
ENTERPRISE_PLAN = 'ENTERPRISE_PLAN', DurationSort = 'DurationSort',
BASIC_PLAN = 'BASIC_PLAN', 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',
} }

View File

@ -0,0 +1,3 @@
export const REACT_QUERY_KEY = {
GET_ALL_LICENCES: 'GET_ALL_LICENCES',
};

View File

@ -18,7 +18,7 @@ import {
UPDATE_CONFIGS, UPDATE_CONFIGS,
UPDATE_CURRENT_ERROR, UPDATE_CURRENT_ERROR,
UPDATE_CURRENT_VERSION, UPDATE_CURRENT_VERSION,
UPDATE_FEATURE_FLAGS, UPDATE_FEATURE_FLAG_RESPONSE,
UPDATE_LATEST_VERSION, UPDATE_LATEST_VERSION,
UPDATE_LATEST_VERSION_ERROR, UPDATE_LATEST_VERSION_ERROR,
} from 'types/actions/app'; } from 'types/actions/app';
@ -129,19 +129,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
message: t('oops_something_went_wrong_version'), 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 ( if (
getUserVersionResponse.isFetched && 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 ( if (
getDynamicConfigsResponse.isFetched && getDynamicConfigsResponse.isFetched &&
getDynamicConfigsResponse.isSuccess && getDynamicConfigsResponse.isSuccess &&
@ -226,6 +199,29 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
notifications, 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; const isToDisplayLayout = isLoggedIn;
return ( return (

View File

@ -36,8 +36,16 @@ export const alertDefaults: AlertDef = {
}, },
}, },
promQueries: {}, promQueries: {},
chQueries: {}, chQueries: {
queryType: EQueryType.QUERY_BUILDER, A: {
name: 'A',
query: ``,
rawQuery: ``,
legend: '',
disabled: false,
},
},
queryType: EQueryType.CLICKHOUSE,
panelType: PANEL_TYPES.TIME_SERIES, panelType: PANEL_TYPES.TIME_SERIES,
}, },
op: defaultCompareOp, op: defaultCompareOp,

View File

@ -2,7 +2,7 @@ import { Button, Tabs } from 'antd';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts'; import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder'; import { QueryBuilder } from 'container/QueryBuilder';
import React from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertTypes } from 'types/api/alerts/alertTypes';
import { IChQueries, IPromQueries } from 'types/api/alerts/compositeQuery'; import { IChQueries, IPromQueries } from 'types/api/alerts/compositeQuery';
@ -91,11 +91,14 @@ function QuerySection({
}, },
]; ];
const items = [ const items = useMemo(
() => [
{ label: t('tab_qb'), key: EQueryType.QUERY_BUILDER }, { label: t('tab_qb'), key: EQueryType.QUERY_BUILDER },
{ label: t('tab_chquery'), key: EQueryType.CLICKHOUSE }, { label: t('tab_chquery'), key: EQueryType.CLICKHOUSE },
{ label: t('tab_promql'), key: EQueryType.PROM }, { label: t('tab_promql'), key: EQueryType.PROM },
]; ],
[t],
);
const renderTabs = (typ: AlertTypes): JSX.Element | null => { const renderTabs = (typ: AlertTypes): JSX.Element | null => {
switch (typ) { switch (typ) {

View File

@ -1,11 +1,13 @@
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons'; 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 saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert'; import testAlertApi from 'api/alerts/testAlert';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag'; import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag'; import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
@ -145,6 +147,7 @@ function FormAlertRules({
// onQueryCategoryChange handles changes to query category // onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults // in state as well as sets additional defaults
const onQueryCategoryChange = (val: EQueryType): void => { const onQueryCategoryChange = (val: EQueryType): void => {
console.log('onQueryCategoryChange', val);
setQueryCategory(val); setQueryCategory(val);
if (val === EQueryType.PROM) { if (val === EQueryType.PROM) {
setAlertDef({ setAlertDef({
@ -298,6 +301,10 @@ function FormAlertRules({
initQuery, initQuery,
]); ]);
const isAlertAvialable = useIsFeatureDisabled(
FeatureKeys.QUERY_BUILDER_ALERTS,
);
const saveRule = useCallback(async () => { const saveRule = useCallback(async () => {
if (!isFormValid()) { if (!isFormValid()) {
return; return;
@ -437,6 +444,12 @@ function FormAlertRules({
selectedInterval={toChartInterval(alertDef.evalWindow)} selectedInterval={toChartInterval(alertDef.evalWindow)}
/> />
); );
const isNewRule = ruleId === 0;
const isAlertAvialableToSave =
isAlertAvialable && isNewRule && queryCategory === EQueryType.QUERY_BUILDER;
return ( return (
<> <>
{Element} {Element}
@ -469,14 +482,18 @@ function FormAlertRules({
{renderBasicInfo()} {renderBasicInfo()}
<ButtonContainer> <ButtonContainer>
<Tooltip title={isAlertAvialableToSave ? MESSAGE.ALERT : ''}>
<ActionButton <ActionButton
loading={loading || false} loading={loading || false}
type="primary" type="primary"
onClick={onSaveHandler} onClick={onSaveHandler}
icon={<SaveOutlined />} icon={<SaveOutlined />}
disabled={isAlertAvialableToSave}
> >
{ruleId > 0 ? t('button_savechanges') : t('button_createrule')} {isNewRule ? t('button_createrule') : t('button_savechanges')}
</ActionButton> </ActionButton>
</Tooltip>
<ActionButton <ActionButton
loading={loading || false} loading={loading || false}
type="default" type="default"

View File

@ -2,12 +2,14 @@ import { Typography } from 'antd';
import { ChartData } from 'chart.js'; import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent'; import GridGraphComponent from 'container/GridGraphComponent';
import { useNotifications } from 'hooks/useNotifications';
import usePreviousValue from 'hooks/usePreviousValue'; import usePreviousValue from 'hooks/usePreviousValue';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import getChartData from 'lib/getChartData'; import getChartData from 'lib/getChartData';
import isEmpty from 'lodash-es/isEmpty'; import isEmpty from 'lodash-es/isEmpty';
import React, { memo, useCallback, useMemo, useState } from 'react'; import React, { memo, useCallback, useMemo, useState } from 'react';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { connect, useSelector } from 'react-redux'; import { connect, useSelector } from 'react-redux';
@ -20,8 +22,8 @@ import {
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults'; import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { GlobalTime } from 'types/actions/globalTime';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards'; import DashboardReducer from 'types/reducer/dashboards';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
@ -46,18 +48,22 @@ function GridCardGraph({
initialInView: true, initialInView: true,
}); });
const { notifications } = useNotifications();
const { t } = useTranslation(['common']);
const [errorMessage, setErrorMessage] = useState<string | undefined>(''); const [errorMessage, setErrorMessage] = useState<string | undefined>('');
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const [modal, setModal] = useState(false); const [modal, setModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false);
const { minTime, maxTime } = useSelector<AppState, GlobalTime>( const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
(state) => state.globalTime,
);
const { selectedTime: globalSelectedInterval } = useSelector<
AppState, AppState,
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const { dashboards } = useSelector<AppState, DashboardReducer>( const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards, (state) => state.dashboards,
); );
@ -122,9 +128,27 @@ function GridCardGraph({
const widgetId = isEmptyWidget ? layout[0].i : widget?.id; const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
featureResponse
.refetch()
.then(() => {
deleteWidget({ widgetId, setLayout }); deleteWidget({ widgetId, setLayout });
onToggleModal(setDeleteModal); onToggleModal(setDeleteModal);
}, [deleteWidget, layout, onToggleModal, setLayout, widget]); })
.catch(() => {
notifications.error({
message: t('common:something_went_wrong'),
});
});
}, [
widget,
layout,
featureResponse,
deleteWidget,
setLayout,
onToggleModal,
notifications,
t,
]);
const getModals = (): JSX.Element => ( const getModals = (): JSX.Element => (
<> <>

View File

@ -1,6 +1,9 @@
import { PlusOutlined, SaveFilled } from '@ant-design/icons'; import { PlusOutlined, SaveFilled } from '@ant-design/icons';
import { Typography } from 'antd';
import { FeatureKeys } from 'constants/features';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import useFeatureFlag, { MESSAGE } from 'hooks/useFeatureFlag';
import React from 'react'; import React from 'react';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@ -14,6 +17,7 @@ import {
ButtonContainer, ButtonContainer,
Card, Card,
CardContainer, CardContainer,
NoPanelAvialable,
ReactGridLayout, ReactGridLayout,
} from './styles'; } from './styles';
@ -35,6 +39,8 @@ function GraphLayout({
role, role,
); );
const queryBuilderFeature = useFeatureFlag(FeatureKeys.QUERY_BUILDER_PANELS);
return ( return (
<> <>
<ButtonContainer> <ButtonContainer>
@ -74,9 +80,31 @@ function GraphLayout({
onLayoutChange={onLayoutChangeHandler} onLayoutChange={onLayoutChangeHandler}
draggableHandle=".drag-handle" draggableHandle=".drag-handle"
> >
{layouts.map(({ Component, ...rest }) => { {layouts.map(({ Component, ...rest }, layoutIndex) => {
const currentWidget = (widgets || [])?.find((e) => e.id === rest.i); 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 ( return (
<CardContainer <CardContainer
isDarkMode={isDarkMode} isDarkMode={isDarkMode}

View File

@ -84,6 +84,8 @@ function GridGraph(props: Props): JSX.Element {
[dispatch], [dispatch],
); );
const { notifications } = useNotifications();
useEffect(() => { useEffect(() => {
(async (): Promise<void> => { (async (): Promise<void> => {
if (!isAddWidget) { if (!isAddWidget) {
@ -119,6 +121,12 @@ function GridGraph(props: Props): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps // 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( const onLayoutSaveHandler = useCallback(
async (layout: Layout[]) => { async (layout: Layout[]) => {
try { try {
@ -128,6 +136,10 @@ function GridGraph(props: Props): JSX.Element {
errorMessage: '', errorMessage: '',
loading: true, loading: true,
})); }));
featureResponse
.refetch()
.then(async () => {
const updatedDashboard: Dashboard = { const updatedDashboard: Dashboard = {
...selectedDashboard, ...selectedDashboard,
data: { data: {
@ -159,13 +171,27 @@ function GridGraph(props: Props): JSX.Element {
setSaveLayoutState((state) => ({ setSaveLayoutState((state) => ({
...state, ...state,
error: true, error: true,
errorMessage: response.error || 'Something went wrong', errorMessage: response.error || errorMessage,
loading: false, loading: false,
})); }));
} }
} }
})
.catch(() => {
setSaveLayoutState((state) => ({
...state,
error: true,
errorMessage,
loading: false,
}));
notifications.error({
message: errorMessage,
});
});
} catch (error) { } catch (error) {
console.error(error); notifications.error({
message: errorMessage,
});
} }
}, },
[ [
@ -176,6 +202,9 @@ function GridGraph(props: Props): JSX.Element {
data.variables, data.variables,
data.widgets, data.widgets,
dispatch, dispatch,
errorMessage,
featureResponse,
notifications,
saveLayoutPermission, saveLayoutPermission,
selectedDashboard, selectedDashboard,
], ],
@ -207,8 +236,6 @@ function GridGraph(props: Props): JSX.Element {
[widgets, onDragSelect], [widgets, onDragSelect],
); );
const { notifications } = useNotifications();
const onEmptyWidgetHandler = useCallback(async () => { const onEmptyWidgetHandler = useCallback(async () => {
try { try {
const id = 'empty'; const id = 'empty';
@ -239,10 +266,10 @@ function GridGraph(props: Props): JSX.Element {
setLayoutFunction(layout); setLayoutFunction(layout);
} catch (error) { } catch (error) {
notifications.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> => { const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
setLayoutFunction(layout); setLayoutFunction(layout);
@ -253,6 +280,9 @@ function GridGraph(props: Props): JSX.Element {
const onAddPanelHandler = useCallback(() => { const onAddPanelHandler = useCallback(() => {
try { try {
setAddPanelLoading(true); setAddPanelLoading(true);
featureResponse
.refetch()
.then(() => {
const isEmptyLayoutPresent = const isEmptyLayoutPresent =
layouts.find((e) => e.i === 'empty') !== undefined; layouts.find((e) => e.i === 'empty') !== undefined;
@ -263,33 +293,44 @@ function GridGraph(props: Props): JSX.Element {
toggleAddWidget(true); toggleAddWidget(true);
}) })
.catch(() => { .catch(() => {
notifications.error(t('something_went_wrong')); notifications.error({
message: errorMessage,
});
}); });
} else { } else {
toggleAddWidget(true); toggleAddWidget(true);
setAddPanelLoading(false); setAddPanelLoading(false);
} }
} catch (error) { })
if (typeof error === 'string') { .catch(() =>
notifications.error({ notifications.error({
message: error || t('something_went_wrong'), message: errorMessage,
}),
);
} catch (error) {
notifications.error({
message: errorMessage,
}); });
} }
} }, [
}, [layouts, onEmptyWidgetHandler, t, toggleAddWidget, notifications]); featureResponse,
layouts,
onEmptyWidgetHandler,
toggleAddWidget,
notifications,
errorMessage,
]);
return ( return (
<GraphLayoutContainer <GraphLayoutContainer
{...{ addPanelLoading={addPanelLoading}
addPanelLoading, layouts={layouts}
layouts, onAddPanelHandler={onAddPanelHandler}
onAddPanelHandler, onLayoutChangeHandler={onLayoutChangeHandler}
onLayoutChangeHandler, onLayoutSaveHandler={onLayoutSaveHandler}
onLayoutSaveHandler, saveLayoutState={saveLayoutState}
saveLayoutState, setLayout={setLayout}
widgets, widgets={widgets}
setLayout,
}}
/> />
); );
} }

View File

@ -79,3 +79,11 @@ export const Button = styled(ButtonComponent)`
align-items: center; align-items: center;
} }
`; `;
export const NoPanelAvialable = styled.div<Props>`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
`;

View File

@ -1,7 +1,6 @@
import { Typography } from 'antd'; import { Spin, Typography } from 'antd';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import useFeatureFlags from 'hooks/useFeatureFlag'; import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import history from 'lib/history'; import history from 'lib/history';
import React from 'react'; import React from 'react';
@ -12,7 +11,22 @@ import {
} from './styles'; } from './styles';
function ManageLicense({ onToggle }: ManageLicenseProps): JSX.Element { 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 ( return (
<> <>
<Typography>SIGNOZ STATUS</Typography> <Typography>SIGNOZ STATUS</Typography>
@ -23,14 +37,7 @@ function ManageLicense({ onToggle }: ManageLicenseProps): JSX.Element {
<Typography>{!isEnterprise ? 'Free Plan' : 'Enterprise Plan'} </Typography> <Typography>{!isEnterprise ? 'Free Plan' : 'Enterprise Plan'} </Typography>
</ManageLicenseWrapper> </ManageLicenseWrapper>
<Typography.Link <Typography.Link onClick={onManageLicense}>Manage Licenses</Typography.Link>
onClick={(): void => {
onToggle();
history.push(ROUTES.LIST_LICENSES);
}}
>
Manage Licenses
</Typography.Link>
</ManageLicenseContainer> </ManageLicenseContainer>
</> </>
); );

View File

@ -1,15 +1,14 @@
import { Button, Form, Input } from 'antd'; import { Button, Form, Input } from 'antd';
import getFeaturesFlags from 'api/features/getFeatureFlags';
import apply from 'api/licenses/apply'; import apply from 'api/licenses/apply';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { QueryObserverResult, RefetchOptions, useQuery } from 'react-query'; import { QueryObserverResult, RefetchOptions } from 'react-query';
import { useDispatch } from 'react-redux'; import { useSelector } from 'react-redux';
import { Dispatch } from 'redux'; import { AppState } from 'store/reducers';
import { AppAction, UPDATE_FEATURE_FLAGS } from 'types/actions/app';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/licenses/getAll'; import { PayloadProps } from 'types/api/licenses/getAll';
import AppReducer from 'types/reducer/app';
import { ApplyForm, ApplyFormContainer, LicenseInput } from './styles'; import { ApplyForm, ApplyFormContainer, LicenseInput } from './styles';
@ -21,12 +20,9 @@ function ApplyLicenseForm({
const { t } = useTranslation(['licenses']); const { t } = useTranslation(['licenses']);
const [key, setKey] = useState(''); const [key, setKey] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const dispatch = useDispatch<Dispatch<AppAction>>(); const { featureResponse } = useSelector<AppState, AppReducer>(
const { refetch } = useQuery({ (state) => state.app,
queryFn: getFeaturesFlags, );
queryKey: 'getFeatureFlags',
enabled: false,
});
const { notifications } = useNotifications(); const { notifications } = useNotifications();
@ -47,16 +43,8 @@ function ApplyLicenseForm({
}); });
if (response.statusCode === 200) { if (response.statusCode === 200) {
const [featureFlagsResponse] = await Promise.all([ await Promise.all([featureResponse?.refetch(), licenseRefetch()]);
refetch(),
licenseRefetch(),
]);
if (featureFlagsResponse.data?.payload) {
dispatch({
type: UPDATE_FEATURE_FLAGS,
payload: featureFlagsResponse.data.payload,
});
}
notifications.success({ notifications.success({
message: 'Success', message: 'Success',
description: t('license_applied'), description: t('license_applied'),

View File

@ -1,19 +1,15 @@
import { Tabs, Typography } from 'antd'; import { Tabs, Typography } from 'antd';
import getAll from 'api/licenses/getAll';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import useLicense from 'hooks/useLicense';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import ApplyLicenseForm from './ApplyLicenseForm'; import ApplyLicenseForm from './ApplyLicenseForm';
import ListLicenses from './ListLicenses'; import ListLicenses from './ListLicenses';
function Licenses(): JSX.Element { function Licenses(): JSX.Element {
const { t } = useTranslation(['licenses']); const { t } = useTranslation(['licenses']);
const { data, isError, isLoading, refetch } = useQuery({ const { data, isError, isLoading, refetch } = useLicense();
queryFn: getAll,
queryKey: 'getAllLicenses',
});
if (isError || data?.error) { if (isError || data?.error) {
return <Typography>{data?.error}</Typography>; return <Typography>{data?.error}</Typography>;

View File

@ -2,8 +2,11 @@ import { NotificationInstance } from 'antd/es/notification/interface';
import deleteAlerts from 'api/alerts/delete'; import deleteAlerts from 'api/alerts/delete';
import { State } from 'hooks/useFetch'; import { State } from 'hooks/useFetch';
import React, { useState } from 'react'; 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 { PayloadProps as DeleteAlertPayloadProps } from 'types/api/alerts/delete';
import { GettableAlert } from 'types/api/alerts/get'; import { GettableAlert } from 'types/api/alerts/get';
import AppReducer from 'types/reducer/app';
import { ColumnButton } from './styles'; import { ColumnButton } from './styles';
@ -22,15 +25,14 @@ function DeleteAlert({
payload: undefined, payload: undefined,
}); });
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const defaultErrorMessage = 'Something went wrong'; const defaultErrorMessage = 'Something went wrong';
const onDeleteHandler = async (id: number): Promise<void> => { const onDeleteHandler = async (id: number): Promise<void> => {
try { try {
setDeleteAlertState((state) => ({
...state,
loading: true,
}));
const response = await deleteAlerts({ const response = await deleteAlerts({
id, 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 ( return (
<ColumnButton <ColumnButton
disabled={deleteAlertState.loading || false} disabled={deleteAlertState.loading || false}
loading={deleteAlertState.loading || false} loading={deleteAlertState.loading || false}
onClick={(): Promise<void> => onDeleteHandler(id)} onClick={onClickHandler}
type="link" type="link"
> >
Delete Delete

View File

@ -26,7 +26,9 @@ import ToggleAlertState from './ToggleAlertState';
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const [data, setData] = useState<GettableAlert[]>(allAlertRules || []); const [data, setData] = useState<GettableAlert[]>(allAlertRules || []);
const { t } = useTranslation('common'); 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( const [addNewAlert, action] = useComponentPermission(
['add_new_alert', 'action'], ['add_new_alert', 'action'],
role, role,
@ -48,12 +50,28 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
})(); })();
}, 30000); }, 30000);
const handleError = useCallback((): void => {
notificationsApi.error({
message: t('something_went_wrong'),
});
}, [notificationsApi, t]);
const onClickNewAlertHandler = useCallback(() => { const onClickNewAlertHandler = useCallback(() => {
featureResponse
.refetch()
.then(() => {
history.push(ROUTES.ALERTS_NEW); history.push(ROUTES.ALERTS_NEW);
}, []); })
.catch(handleError);
}, [featureResponse, handleError]);
const onEditHandler = (id: string): void => { const onEditHandler = (id: string): void => {
featureResponse
.refetch()
.then(() => {
history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${id}`); history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${id}`);
})
.catch(handleError);
}; };
const columns: ColumnsType<GettableAlert> = [ const columns: ColumnsType<GettableAlert> = [

View File

@ -4,6 +4,7 @@ import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd';
import createDashboard from 'api/dashboard/create'; import createDashboard from 'api/dashboard/create';
import Editor from 'components/Editor'; import Editor from 'components/Editor';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { MESSAGE } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
import React, { useState } from 'react'; import React, { useState } from 'react';
@ -28,6 +29,8 @@ function ImportJSON({
const [isCreateDashboardError, setIsCreateDashboardError] = useState<boolean>( const [isCreateDashboardError, setIsCreateDashboardError] = useState<boolean>(
false, false,
); );
const [isFeatureAlert, setIsFeatureAlert] = useState<boolean>(false);
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
const [dashboardCreating, setDashboardCreating] = useState<boolean>(false); const [dashboardCreating, setDashboardCreating] = useState<boolean>(false);
@ -99,6 +102,15 @@ function ImportJSON({
}), }),
); );
}, 10); }, 10);
} else if (response.error === 'feature usage exceeded') {
setIsFeatureAlert(true);
notifications.error({
message:
response.error ||
t('something_went_wrong', {
ns: 'common',
}),
});
} else { } else {
setIsCreateDashboardError(true); setIsCreateDashboardError(true);
notifications.error({ notifications.error({
@ -112,6 +124,7 @@ function ImportJSON({
setDashboardCreating(false); setDashboardCreating(false);
} catch { } catch {
setDashboardCreating(false); setDashboardCreating(false);
setIsFeatureAlert(false);
setIsCreateDashboardError(true); setIsCreateDashboardError(true);
} }
@ -124,6 +137,13 @@ function ImportJSON({
</Space> </Space>
); );
const onCancelHandler = (): void => {
setIsUploadJSONError(false);
setIsCreateDashboardError(false);
setIsFeatureAlert(false);
onModalHandler();
};
return ( return (
<Modal <Modal
open={isImportJSONModalVisible} open={isImportJSONModalVisible}
@ -131,7 +151,7 @@ function ImportJSON({
maskClosable maskClosable
destroyOnClose destroyOnClose
width="70vw" width="70vw"
onCancel={onModalHandler} onCancel={onCancelHandler}
title={ title={
<> <>
<Typography.Title level={4}>{t('import_json')}</Typography.Title> <Typography.Title level={4}>{t('import_json')}</Typography.Title>
@ -148,6 +168,11 @@ function ImportJSON({
{t('load_json')} {t('load_json')}
</Button> </Button>
{isCreateDashboardError && getErrorNode(t('error_loading_json'))} {isCreateDashboardError && getErrorNode(t('error_loading_json'))}
{isFeatureAlert && (
<Typography.Text type="danger">
{MESSAGE.CREATE_DASHBOARD}
</Typography.Text>
)}
</FooterContainer> </FooterContainer>
} }
> >

View File

@ -74,7 +74,8 @@ function ListOfAllDashboard(): JSX.Element {
errorMessage: '', errorMessage: '',
}); });
const columns: TableColumnProps<Data>[] = [ const columns: TableColumnProps<Data>[] = useMemo(
() => [
{ {
title: 'Name', title: 'Name',
dataIndex: 'name', dataIndex: 'name',
@ -116,7 +117,9 @@ function ListOfAllDashboard(): JSX.Element {
}, },
render: DateComponent, render: DateComponent,
}, },
]; ],
[],
);
if (action) { if (action) {
columns.push({ columns.push({
@ -199,7 +202,7 @@ function ListOfAllDashboard(): JSX.Element {
setUploadedGrafana(uploadedGrafana); setUploadedGrafana(uploadedGrafana);
}; };
const getMenuItems = useCallback(() => { const getMenuItems = useMemo(() => {
const menuItems: ItemType[] = []; const menuItems: ItemType[] = [];
if (createNewDashboard) { if (createNewDashboard) {
menuItems.push({ menuItems.push({
@ -227,7 +230,7 @@ function ListOfAllDashboard(): JSX.Element {
const menu: MenuProps = useMemo( const menu: MenuProps = useMemo(
() => ({ () => ({
items: getMenuItems(), items: getMenuItems,
}), }),
[getMenuItems], [getMenuItems],
); );
@ -245,7 +248,7 @@ function ListOfAllDashboard(): JSX.Element {
}} }}
/> />
{newDashboard && ( {newDashboard && (
<Dropdown trigger={['click']} menu={menu}> <Dropdown disabled={loading} trigger={['click']} menu={menu}>
<NewDashboardButton <NewDashboardButton
icon={<PlusOutlined />} icon={<PlusOutlined />}
type="primary" type="primary"
@ -260,11 +263,12 @@ function ListOfAllDashboard(): JSX.Element {
</Row> </Row>
), ),
[ [
getText,
newDashboard, newDashboard,
newDashboardState.error, loading,
newDashboardState.loading,
menu, menu,
newDashboardState.loading,
newDashboardState.error,
getText,
], ],
); );

View File

@ -34,10 +34,12 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
CLICKHOUSE: uuid(), CLICKHOUSE: uuid(),
PROM: uuid(), PROM: uuid(),
}); });
const { dashboards, isLoadingQueryResult } = useSelector< const { dashboards, isLoadingQueryResult } = useSelector<
AppState, AppState,
DashboardReducer DashboardReducer
>((state) => state.dashboards); >((state) => state.dashboards);
const [selectedDashboards] = dashboards; const [selectedDashboards] = dashboards;
const { search } = useLocation(); const { search } = useLocation();
const { widgets } = selectedDashboards.data; const { widgets } = selectedDashboards.data;

View File

@ -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 ROUTES from 'constants/routes';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; 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 { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import history from 'lib/history'; import history from 'lib/history';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget'; import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
@ -22,6 +26,7 @@ import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { FLUSH_DASHBOARD } from 'types/actions/dashboard'; import { FLUSH_DASHBOARD } from 'types/actions/dashboard';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards'; import DashboardReducer from 'types/reducer/dashboards';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
@ -51,6 +56,10 @@ function NewWidget({
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const [selectedDashboard] = dashboards; const [selectedDashboard] = dashboards;
const { widgets } = selectedDashboard.data; const { widgets } = selectedDashboard.data;
@ -99,8 +108,13 @@ function NewWidget({
enum: selectedWidget?.timePreferance || 'GLOBAL_TIME', enum: selectedWidget?.timePreferance || 'GLOBAL_TIME',
}); });
const { notifications } = useNotifications();
const onClickSaveHandler = useCallback(() => { const onClickSaveHandler = useCallback(() => {
// update the global state // update the global state
featureResponse
.refetch()
.then(() => {
saveSettingOfPanel({ saveSettingOfPanel({
uuid: selectedDashboard.uuid, uuid: selectedDashboard.uuid,
description, description,
@ -114,7 +128,14 @@ function NewWidget({
dashboardId, dashboardId,
graphType, graphType,
}); });
})
.catch(() => {
notifications.error({
message: 'Something went wrong',
});
});
}, [ }, [
featureResponse,
saveSettingOfPanel, saveSettingOfPanel,
selectedDashboard.uuid, selectedDashboard.uuid,
description, description,
@ -127,6 +148,7 @@ function NewWidget({
query, query,
dashboardId, dashboardId,
graphType, graphType,
notifications,
]); ]);
const onClickDiscardHandler = useCallback(() => { const onClickDiscardHandler = useCallback(() => {
@ -167,13 +189,39 @@ function NewWidget({
getQueryResult(); getQueryResult();
}, [getQueryResult]); }, [getQueryResult]);
const onSaveDashboard = useCallback((): void => {
setSaveModal(true);
}, []);
const isQueryBuilderActive = useIsFeatureDisabled(
FeatureKeys.QUERY_BUILDER_PANELS,
);
return ( return (
<Container> <Container>
<ButtonContainer> <ButtonContainer>
<Button type="primary" onClick={(): void => setSaveModal(true)}> {isQueryBuilderActive && (
<Tooltip title={MESSAGE.PANEL}>
<Button
icon={<LockFilled />}
type="primary"
disabled={isQueryBuilderActive}
onClick={onSaveDashboard}
>
Save Save
</Button> </Button>
{/* <Button onClick={onClickApplyHandler}>Apply</Button> */} </Tooltip>
)}
{!isQueryBuilderActive && (
<Button
type="primary"
disabled={isQueryBuilderActive}
onClick={onSaveDashboard}
>
Save
</Button>
)}
<Button onClick={onClickDiscardHandler}>Discard</Button> <Button onClick={onClickDiscardHandler}>Discard</Button>
</ButtonContainer> </ButtonContainer>

View File

@ -3,8 +3,8 @@ import { PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Modal, Typography } from 'antd'; import { Button, Form, Input, Modal, Typography } from 'antd';
import { useForm } from 'antd/es/form/Form'; import { useForm } from 'antd/es/form/Form';
import createDomainApi from 'api/SAML/postDomain'; import createDomainApi from 'api/SAML/postDomain';
import { FeatureKeys } from 'constants/featureKeys'; import { FeatureKeys } from 'constants/features';
import useFeatureFlag from 'hooks/useFeatureFlag'; import useFeatureFlag from 'hooks/useFeatureFlag/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -18,7 +18,7 @@ function AddDomain({ refetch }: Props): JSX.Element {
const { t } = useTranslation(['common', 'organizationsettings']); const { t } = useTranslation(['common', 'organizationsettings']);
const [isAddDomains, setIsDomain] = useState(false); const [isAddDomains, setIsDomain] = useState(false);
const [form] = useForm<FormProps>(); const [form] = useForm<FormProps>();
const SSOFlag = useFeatureFlag(FeatureKeys.SSO); const isSsoFlagEnabled = useFeatureFlag(FeatureKeys.SSO);
const { org } = useSelector<AppState, AppReducer>((state) => state.app); const { org } = useSelector<AppState, AppReducer>((state) => state.app);
@ -58,7 +58,7 @@ function AddDomain({ refetch }: Props): JSX.Element {
ns: 'organizationsettings', ns: 'organizationsettings',
})} })}
</Typography.Title> </Typography.Title>
{SSOFlag && ( {isSsoFlagEnabled && (
<Button <Button
onClick={(): void => setIsDomain(true)} onClick={(): void => setIsDomain(true)}
type="primary" type="primary"

View File

@ -7,8 +7,8 @@ import updateDomain from 'api/SAML/updateDomain';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import { SIGNOZ_UPGRADE_PLAN_URL } from 'constants/app'; import { SIGNOZ_UPGRADE_PLAN_URL } from 'constants/app';
import { FeatureKeys } from 'constants/featureKeys'; import { FeatureKeys } from 'constants/features';
import useFeatureFlag from 'hooks/useFeatureFlag'; import useFeatureFlag from 'hooks/useFeatureFlag/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';

View File

@ -1,6 +1,6 @@
import { Divider, Space } from 'antd'; import { Divider, Space } from 'antd';
import { FeatureKeys } from 'constants/featureKeys'; import { FeatureKeys } from 'constants/features';
import useFeatureFlag from 'hooks/useFeatureFlag'; import { useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -14,8 +14,11 @@ import PendingInvitesContainer from './PendingInvitesContainer';
function OrganizationSettings(): JSX.Element { function OrganizationSettings(): JSX.Element {
const { org } = useSelector<AppState, AppReducer>((state) => state.app); const { org } = useSelector<AppState, AppReducer>((state) => state.app);
const sso = useFeatureFlag(FeatureKeys.SSO); const isNotSSO = useIsFeatureDisabled(FeatureKeys.SSO);
const noUpsell = useFeatureFlag(FeatureKeys.DISABLE_UPSELL);
const isNoUpSell = useIsFeatureDisabled(FeatureKeys.DISABLE_UPSELL);
const isAuthDomain = !isNoUpSell || (isNoUpSell && !isNotSSO);
if (!org) { if (!org) {
return <div />; return <div />;
@ -38,7 +41,7 @@ function OrganizationSettings(): JSX.Element {
<Divider /> <Divider />
<Members /> <Members />
<Divider /> <Divider />
{(!noUpsell || (noUpsell && sso)) && <AuthDomains />} {isAuthDomain && <AuthDomains />}
</> </>
); );
} }

View File

@ -16,21 +16,6 @@ import { useSetCurrentKeyAndOperator } from './useSetCurrentKeyAndOperator';
import { useTag } from './useTag'; import { useTag } from './useTag';
import { useTagValidation } from './useTagValidation'; 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 => { export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
const [searchValue, setSearchValue] = useState<string>(''); const [searchValue, setSearchValue] = useState<string>('');
const [searchKey, setSearchKey] = useState<string>(''); const [searchKey, setSearchKey] = useState<string>('');
@ -134,3 +119,18 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
searchKey, 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;
}

View File

@ -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;

View 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.',
};

View File

@ -0,0 +1,7 @@
import { MESSAGE } from './constant';
import useFeatureFlag from './useFeatureFlag';
import useIsFeatureDisabled from './useIsFeatureDisabled';
export default useFeatureFlag;
export { MESSAGE, useIsFeatureDisabled };

View 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;

View 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;

View File

@ -0,0 +1,3 @@
export const LICENSE_PLAN_KEY = {
ENTERPRISE_PLAN: 'ENTERPRISE_PLAN',
};

View File

@ -0,0 +1,6 @@
import { LICENSE_PLAN_KEY } from './constant';
import useLicense from './useLicense';
export default useLicense;
export { LICENSE_PLAN_KEY };

View 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;

View File

@ -9,7 +9,7 @@ import {
UPDATE_CONFIGS, UPDATE_CONFIGS,
UPDATE_CURRENT_ERROR, UPDATE_CURRENT_ERROR,
UPDATE_CURRENT_VERSION, UPDATE_CURRENT_VERSION,
UPDATE_FEATURE_FLAGS, UPDATE_FEATURE_FLAG_RESPONSE,
UPDATE_LATEST_VERSION, UPDATE_LATEST_VERSION,
UPDATE_LATEST_VERSION_ERROR, UPDATE_LATEST_VERSION_ERROR,
UPDATE_ORG, UPDATE_ORG,
@ -47,7 +47,10 @@ const InitialValue: InitialValueTypes = {
isSideBarCollapsed: getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true', isSideBarCollapsed: getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
currentVersion: '', currentVersion: '',
latestVersion: '', latestVersion: '',
featureFlags: {}, featureResponse: {
data: null,
refetch: Promise.resolve,
},
isCurrentVersionError: false, isCurrentVersionError: false,
isLatestVersionError: false, isLatestVersionError: false,
user: getInitialUser(), user: getInitialUser(),
@ -80,10 +83,13 @@ const appReducer = (
}; };
} }
case UPDATE_FEATURE_FLAGS: { case UPDATE_FEATURE_FLAG_RESPONSE: {
return { return {
...state, ...state,
featureFlags: { ...action.payload }, featureResponse: {
data: action.payload.featureFlag,
refetch: action.payload.refetch,
},
}; };
} }

View File

@ -1,3 +1,4 @@
import { QueryObserverBaseResult } from 'react-query';
import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags'; import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags';
import { import {
Organization, Organization,
@ -22,9 +23,9 @@ export const UPDATE_USER_ORG_ROLE = 'UPDATE_USER_ORG_ROLE';
export const UPDATE_USER = 'UPDATE_USER'; export const UPDATE_USER = 'UPDATE_USER';
export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME'; export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME';
export const UPDATE_ORG = 'UPDATE_ORG'; export const UPDATE_ORG = 'UPDATE_ORG';
export const UPDATE_FEATURE_FLAGS = 'UPDATE_FEATURE_FLAGS';
export const UPDATE_CONFIGS = 'UPDATE_CONFIGS'; export const UPDATE_CONFIGS = 'UPDATE_CONFIGS';
export const UPDATE_USER_FLAG = 'UPDATE_USER_FLAG'; export const UPDATE_USER_FLAG = 'UPDATE_USER_FLAG';
export const UPDATE_FEATURE_FLAG_RESPONSE = 'UPDATE_FEATURE_FLAG_RESPONSE';
export interface LoggedInUser { export interface LoggedInUser {
type: typeof LOGGED_IN; type: typeof LOGGED_IN;
@ -38,10 +39,6 @@ export interface SideBarCollapse {
payload: boolean; payload: boolean;
} }
export interface UpdateFeatureFlags {
type: typeof UPDATE_FEATURE_FLAGS;
payload: null | FeatureFlagPayload;
}
export interface UpdateAppVersion { export interface UpdateAppVersion {
type: typeof UPDATE_CURRENT_VERSION; type: typeof UPDATE_CURRENT_VERSION;
payload: { 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 = export type AppAction =
| LoggedInUser | LoggedInUser
| SideBarCollapse | SideBarCollapse
@ -142,6 +147,6 @@ export type AppAction =
| UpdateUser | UpdateUser
| UpdateOrgName | UpdateOrgName
| UpdateOrg | UpdateOrg
| UpdateFeatureFlags
| UpdateConfigs | UpdateConfigs
| UpdateUserFlag; | UpdateUserFlag
| UpdateFeatureFlag;

View File

@ -1,3 +0,0 @@
export interface PayloadProps {
[key: string]: boolean;
}

View File

@ -1,3 +1,19 @@
export interface PayloadProps { export type FeaturesFlag =
[key: string]: boolean; | '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[];

View File

@ -1,3 +1,4 @@
import { QueryObserverBaseResult } from 'react-query';
import { PayloadProps as ConfigPayload } from 'types/api/dynamicConfigs/getDynamicConfigs'; import { PayloadProps as ConfigPayload } from 'types/api/dynamicConfigs/getDynamicConfigs';
import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags'; import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags';
import { PayloadProps as OrgPayload } from 'types/api/user/getOrganization'; import { PayloadProps as OrgPayload } from 'types/api/user/getOrganization';
@ -26,9 +27,12 @@ export default interface AppReducer {
isUserFetchingError: boolean; isUserFetchingError: boolean;
role: ROLES | null; role: ROLES | null;
org: OrgPayload | null; org: OrgPayload | null;
featureFlags: null | FeatureFlagPayload;
configs: ConfigPayload; configs: ConfigPayload;
userFlags: null | UserFlags; userFlags: null | UserFlags;
ee: 'Y' | 'N'; ee: 'Y' | 'N';
setupCompleted: boolean; setupCompleted: boolean;
featureResponse: {
data: FeatureFlagPayload | null;
refetch: QueryObserverBaseResult['refetch'];
};
} }