From 604d98be05003afa8449354bc92f0ae152e75851 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Fri, 19 May 2023 12:19:42 +0530 Subject: [PATCH] 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 --- frontend/src/api/features/getFeatures.ts | 23 --- frontend/src/constants/featureKeys.ts | 7 - frontend/src/constants/features.ts | 9 +- frontend/src/constants/reactQueryKeys.ts | 3 + frontend/src/container/AppLayout/index.tsx | 52 +++--- .../src/container/CreateAlertRule/defaults.ts | 12 +- .../container/FormAlertRules/QuerySection.tsx | 15 +- .../src/container/FormAlertRules/index.tsx | 35 +++- .../container/GridGraphLayout/Graph/index.tsx | 40 +++- .../container/GridGraphLayout/GraphLayout.tsx | 30 ++- .../src/container/GridGraphLayout/index.tsx | 173 +++++++++++------- .../src/container/GridGraphLayout/styles.ts | 8 + .../container/Header/ManageLicense/index.tsx | 31 ++-- .../container/Licenses/ApplyLicenseForm.tsx | 30 +-- frontend/src/container/Licenses/index.tsx | 8 +- .../container/ListAlertRules/DeleteAlert.tsx | 35 +++- .../container/ListAlertRules/ListAlert.tsx | 26 ++- .../ListOfDashboard/ImportJSON/index.tsx | 27 ++- .../src/container/ListOfDashboard/index.tsx | 98 +++++----- .../LeftContainer/QuerySection/index.tsx | 2 + frontend/src/container/NewWidget/index.tsx | 84 +++++++-- .../AuthDomains/AddDomain/index.tsx | 8 +- .../AuthDomains/index.tsx | 4 +- .../container/OrganizationSettings/index.tsx | 13 +- .../src/hooks/queryBuilder/useAutoComplete.ts | 30 +-- frontend/src/hooks/useFeatureFlag.ts | 13 -- frontend/src/hooks/useFeatureFlag/constant.ts | 9 + frontend/src/hooks/useFeatureFlag/index.ts | 7 + .../hooks/useFeatureFlag/useFeatureFlag.ts | 29 +++ .../useFeatureFlag/useIsFeatureDisabled.ts | 11 ++ frontend/src/hooks/useLicense/constant.ts | 3 + frontend/src/hooks/useLicense/index.ts | 6 + frontend/src/hooks/useLicense/useLicense.tsx | 25 +++ frontend/src/store/reducers/app.ts | 14 +- frontend/src/types/actions/app.ts | 19 +- .../src/types/api/features/getFeatures.ts | 3 - .../types/api/features/getFeaturesFlags.ts | 20 +- frontend/src/types/reducer/app.ts | 6 +- 38 files changed, 645 insertions(+), 323 deletions(-) delete mode 100644 frontend/src/api/features/getFeatures.ts delete mode 100644 frontend/src/constants/featureKeys.ts create mode 100644 frontend/src/constants/reactQueryKeys.ts delete mode 100644 frontend/src/hooks/useFeatureFlag.ts create mode 100644 frontend/src/hooks/useFeatureFlag/constant.ts create mode 100644 frontend/src/hooks/useFeatureFlag/index.ts create mode 100644 frontend/src/hooks/useFeatureFlag/useFeatureFlag.ts create mode 100644 frontend/src/hooks/useFeatureFlag/useIsFeatureDisabled.ts create mode 100644 frontend/src/hooks/useLicense/constant.ts create mode 100644 frontend/src/hooks/useLicense/index.ts create mode 100644 frontend/src/hooks/useLicense/useLicense.tsx delete mode 100644 frontend/src/types/api/features/getFeatures.ts diff --git a/frontend/src/api/features/getFeatures.ts b/frontend/src/api/features/getFeatures.ts deleted file mode 100644 index ca6bf30ca7..0000000000 --- a/frontend/src/api/features/getFeatures.ts +++ /dev/null @@ -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 | 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; diff --git a/frontend/src/constants/featureKeys.ts b/frontend/src/constants/featureKeys.ts deleted file mode 100644 index 6684f3ddae..0000000000 --- a/frontend/src/constants/featureKeys.ts +++ /dev/null @@ -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', -} diff --git a/frontend/src/constants/features.ts b/frontend/src/constants/features.ts index ee7d323b30..cceaf2817b 100644 --- a/frontend/src/constants/features.ts +++ b/frontend/src/constants/features.ts @@ -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', } diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts new file mode 100644 index 0000000000..7cc38c153e --- /dev/null +++ b/frontend/src/constants/reactQueryKeys.ts @@ -0,0 +1,3 @@ +export const REACT_QUERY_KEY = { + GET_ALL_LICENCES: 'GET_ALL_LICENCES', +}; diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index c31a2bf6f9..918877747e 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -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 ( diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts index 2b85030035..2b85a052b2 100644 --- a/frontend/src/container/CreateAlertRule/defaults.ts +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -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, diff --git a/frontend/src/container/FormAlertRules/QuerySection.tsx b/frontend/src/container/FormAlertRules/QuerySection.tsx index 0855c52bc0..b3337dfc09 100644 --- a/frontend/src/container/FormAlertRules/QuerySection.tsx +++ b/frontend/src/container/FormAlertRules/QuerySection.tsx @@ -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) { diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index 70ec5c2314..8866956da2 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -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()} - } - > - {ruleId > 0 ? t('button_savechanges') : t('button_createrule')} - + + } + disabled={isAlertAvialableToSave} + > + {isNewRule ? t('button_createrule') : t('button_savechanges')} + + + (''); const [hovered, setHovered] = useState(false); const [modal, setModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false); - const { minTime, maxTime } = useSelector( - (state) => state.globalTime, - ); - const { selectedTime: globalSelectedInterval } = useSelector< + const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< AppState, GlobalReducer >((state) => state.globalTime); + const { featureResponse } = useSelector( + (state) => state.app, + ); const { dashboards } = useSelector( (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 => ( <> diff --git a/frontend/src/container/GridGraphLayout/GraphLayout.tsx b/frontend/src/container/GridGraphLayout/GraphLayout.tsx index d615fb1a13..6ff958b433 100644 --- a/frontend/src/container/GridGraphLayout/GraphLayout.tsx +++ b/frontend/src/container/GridGraphLayout/GraphLayout.tsx @@ -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 ( <> @@ -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 ( + + + + + {MESSAGE.WIDGET.replace('{{widget}}', usageLimit.toString())} + + + + + ); + } + return ( { (async (): Promise => { if (!isAddWidget) { @@ -119,6 +121,12 @@ function GridGraph(props: Props): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const { featureResponse } = useSelector( + (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 => { 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 ( ); } diff --git a/frontend/src/container/GridGraphLayout/styles.ts b/frontend/src/container/GridGraphLayout/styles.ts index 9bb4b219bb..b4d1b6f60b 100644 --- a/frontend/src/container/GridGraphLayout/styles.ts +++ b/frontend/src/container/GridGraphLayout/styles.ts @@ -79,3 +79,11 @@ export const Button = styled(ButtonComponent)` align-items: center; } `; + +export const NoPanelAvialable = styled.div` + display: flex; + justify-content: center; + align-items: center; + + height: 100%; +`; diff --git a/frontend/src/container/Header/ManageLicense/index.tsx b/frontend/src/container/Header/ManageLicense/index.tsx index 37c776ce2a..3eb4374f26 100644 --- a/frontend/src/container/Header/ManageLicense/index.tsx +++ b/frontend/src/container/Header/ManageLicense/index.tsx @@ -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 ; + } + + const isEnterprise = data?.payload?.some( + (license) => + license.isCurrent && license.planKey === LICENSE_PLAN_KEY.ENTERPRISE_PLAN, + ); + return ( <> SIGNOZ STATUS @@ -23,14 +37,7 @@ function ManageLicense({ onToggle }: ManageLicenseProps): JSX.Element { {!isEnterprise ? 'Free Plan' : 'Enterprise Plan'} - { - onToggle(); - history.push(ROUTES.LIST_LICENSES); - }} - > - Manage Licenses - + Manage Licenses ); diff --git a/frontend/src/container/Licenses/ApplyLicenseForm.tsx b/frontend/src/container/Licenses/ApplyLicenseForm.tsx index b729a310a7..e575615e4f 100644 --- a/frontend/src/container/Licenses/ApplyLicenseForm.tsx +++ b/frontend/src/container/Licenses/ApplyLicenseForm.tsx @@ -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>(); - const { refetch } = useQuery({ - queryFn: getFeaturesFlags, - queryKey: 'getFeatureFlags', - enabled: false, - }); + const { featureResponse } = useSelector( + (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'), diff --git a/frontend/src/container/Licenses/index.tsx b/frontend/src/container/Licenses/index.tsx index d7dc4ab22b..be4f9d363d 100644 --- a/frontend/src/container/Licenses/index.tsx +++ b/frontend/src/container/Licenses/index.tsx @@ -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 {data?.error}; diff --git a/frontend/src/container/ListAlertRules/DeleteAlert.tsx b/frontend/src/container/ListAlertRules/DeleteAlert.tsx index 8ff3927d68..83d8badf2d 100644 --- a/frontend/src/container/ListAlertRules/DeleteAlert.tsx +++ b/frontend/src/container/ListAlertRules/DeleteAlert.tsx @@ -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( + (state) => state.app, + ); + const defaultErrorMessage = 'Something went wrong'; const onDeleteHandler = async (id: number): Promise => { 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 ( => onDeleteHandler(id)} + onClick={onClickHandler} type="link" > Delete diff --git a/frontend/src/container/ListAlertRules/ListAlert.tsx b/frontend/src/container/ListAlertRules/ListAlert.tsx index be54f0e914..a544899acd 100644 --- a/frontend/src/container/ListAlertRules/ListAlert.tsx +++ b/frontend/src/container/ListAlertRules/ListAlert.tsx @@ -26,7 +26,9 @@ import ToggleAlertState from './ToggleAlertState'; function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { const [data, setData] = useState(allAlertRules || []); const { t } = useTranslation('common'); - const { role } = useSelector((state) => state.app); + const { role, featureResponse } = useSelector( + (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 = [ diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index 75fa6a581d..c23e8d1d5a 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -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( false, ); + const [isFeatureAlert, setIsFeatureAlert] = useState(false); + const dispatch = useDispatch>(); const [dashboardCreating, setDashboardCreating] = useState(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({ ); + const onCancelHandler = (): void => { + setIsUploadJSONError(false); + setIsCreateDashboardError(false); + setIsFeatureAlert(false); + onModalHandler(); + }; + return ( {t('import_json')} @@ -148,6 +168,11 @@ function ImportJSON({ {t('load_json')} {isCreateDashboardError && getErrorNode(t('error_loading_json'))} + {isFeatureAlert && ( + + {MESSAGE.CREATE_DASHBOARD} + + )} } > diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index ca5d68d9bb..fe96ca4bfd 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -74,49 +74,52 @@ function ListOfAllDashboard(): JSX.Element { errorMessage: '', }); - const columns: TableColumnProps[] = [ - { - 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[] = 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 && ( - + } type="primary" @@ -260,11 +263,12 @@ function ListOfAllDashboard(): JSX.Element { ), [ - getText, newDashboard, - newDashboardState.error, - newDashboardState.loading, + loading, menu, + newDashboardState.loading, + newDashboardState.error, + getText, ], ); diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx index 933410ca81..668a2c20fc 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -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; diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 1264c60ed0..8e71dc6bf3 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -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( + (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 ( - - {/* */} + {isQueryBuilderActive && ( + + + + )} + + {!isQueryBuilderActive && ( + + )} diff --git a/frontend/src/container/OrganizationSettings/AuthDomains/AddDomain/index.tsx b/frontend/src/container/OrganizationSettings/AuthDomains/AddDomain/index.tsx index fa317c8a51..b64c29b7e9 100644 --- a/frontend/src/container/OrganizationSettings/AuthDomains/AddDomain/index.tsx +++ b/frontend/src/container/OrganizationSettings/AuthDomains/AddDomain/index.tsx @@ -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(); - const SSOFlag = useFeatureFlag(FeatureKeys.SSO); + const isSsoFlagEnabled = useFeatureFlag(FeatureKeys.SSO); const { org } = useSelector((state) => state.app); @@ -58,7 +58,7 @@ function AddDomain({ refetch }: Props): JSX.Element { ns: 'organizationsettings', })} - {SSOFlag && ( + {isSsoFlagEnabled && (