diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx index 43402fdbb2..3956676ec7 100644 --- a/frontend/src/AppRoutes/Private.tsx +++ b/frontend/src/AppRoutes/Private.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ import getLocalStorageApi from 'api/browser/localstorage/get'; +import getOrgUser from 'api/user/getOrgUser'; import loginApi from 'api/user/login'; import { Logout } from 'api/utils'; import Spinner from 'components/Spinner'; @@ -8,8 +9,10 @@ import ROUTES from 'constants/routes'; import useLicense from 'hooks/useLicense'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import { ReactChild, useEffect, useMemo } from 'react'; +import { isEmpty } from 'lodash-es'; +import { ReactChild, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useQuery } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { matchPath, Redirect, useLocation } from 'react-router-dom'; import { Dispatch } from 'redux'; @@ -17,6 +20,7 @@ import { AppState } from 'store/reducers'; import { getInitialUserTokenRefreshToken } from 'store/utils'; import AppActions from 'types/actions'; import { UPDATE_USER_IS_FETCH } from 'types/actions/app'; +import { Organization } from 'types/api/user/getOrganization'; import AppReducer from 'types/reducer/app'; import { routePermission } from 'utils/permission'; @@ -31,6 +35,14 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { const location = useLocation(); const { pathname } = location; + const { org, orgPreferences } = useSelector( + (state) => state.app, + ); + + const [isOnboardingComplete, setIsOnboardingComplete] = useState< + boolean | null + >(null); + const mapRoutes = useMemo( () => new Map( @@ -44,6 +56,18 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { [pathname], ); + useEffect(() => { + if (orgPreferences && !isEmpty(orgPreferences)) { + const onboardingPreference = orgPreferences?.find( + (preference: Record) => preference.key === 'ORG_ONBOARDING', + ); + + if (onboardingPreference) { + setIsOnboardingComplete(onboardingPreference.value); + } + } + }, [orgPreferences]); + const { data: licensesData, isFetching: isFetchingLicensesData, @@ -53,9 +77,11 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { isUserFetching, isUserFetchingError, isLoggedIn: isLoggedInState, + isFetchingOrgPreferences, } = useSelector((state) => state.app); const { t } = useTranslation(['common']); + const localStorageUserAuthToken = getInitialUserTokenRefreshToken(); const dispatch = useDispatch>(); @@ -66,6 +92,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { const isOldRoute = oldRoutes.indexOf(pathname) > -1; + const [orgData, setOrgData] = useState(undefined); + const isLocalStorageLoggedIn = getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true'; @@ -81,6 +109,42 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { } }; + const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({ + queryFn: () => { + if (orgData && orgData.id !== undefined) { + return getOrgUser({ + orgId: orgData.id, + }); + } + return undefined; + }, + queryKey: ['getOrgUser'], + enabled: !isEmpty(orgData), + }); + + const checkFirstTimeUser = (): boolean => { + const users = orgUsers?.payload || []; + + const remainingUsers = users.filter( + (user) => user.email !== 'admin@signoz.cloud', + ); + + return remainingUsers.length === 1; + }; + + // Check if the onboarding should be shown based on the org users and onboarding completion status, wait for org users and preferences to load + const shouldShowOnboarding = (): boolean => { + // Only run this effect if the org users and preferences are loaded + if (!isLoadingOrgUsers) { + const isFirstUser = checkFirstTimeUser(); + + // Redirect to get started if it's not the first user or if the onboarding is complete + return isFirstUser && !isOnboardingComplete; + } + + return false; + }; + const handleUserLoginIfTokenPresent = async ( key: keyof typeof ROUTES, ): Promise => { @@ -102,6 +166,16 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { response.payload.refreshJwt, ); + const showOnboarding = shouldShowOnboarding(); + + if ( + userResponse && + showOnboarding && + userResponse.payload.role === 'ADMIN' + ) { + history.push(ROUTES.ONBOARDING); + } + if ( userResponse && route && @@ -160,6 +234,35 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { } }, [isFetchingLicensesData]); + useEffect(() => { + if (org && org.length > 0 && org[0].id !== undefined) { + setOrgData(org[0]); + } + }, [org]); + + const handleRouting = (): void => { + const showOnboarding = shouldShowOnboarding(); + + if (showOnboarding) { + history.push(ROUTES.ONBOARDING); + } else { + history.push(ROUTES.APPLICATION); + } + }; + + useEffect(() => { + // Only run this effect if the org users and preferences are loaded + if (!isLoadingOrgUsers && isOnboardingComplete !== null) { + const isFirstUser = checkFirstTimeUser(); + + // Redirect to get started if it's not the first user or if the onboarding is complete + if (isFirstUser && !isOnboardingComplete) { + history.push(ROUTES.ONBOARDING); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoadingOrgUsers, isOnboardingComplete, orgUsers]); + // eslint-disable-next-line sonarjs/cognitive-complexity useEffect(() => { (async (): Promise => { @@ -183,7 +286,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { // no need to fetch the user and make user fetching false if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') { - history.push(ROUTES.APPLICATION); + handleRouting(); } dispatch({ type: UPDATE_USER_IS_FETCH, @@ -195,7 +298,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { } else if (pathname === ROUTES.HOME_PAGE) { // routing to application page over root page if (isLoggedInState) { - history.push(ROUTES.APPLICATION); + handleRouting(); } else { navigateToLoginIfNotLoggedIn(); } @@ -214,7 +317,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { return ; } - if (isUserFetching) { + if (isUserFetching || (isLoggedInState && isFetchingOrgPreferences)) { return ; } diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index 8cb2bf0a8b..cf17fd4db3 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -2,6 +2,7 @@ import { ConfigProvider } from 'antd'; import getLocalStorageApi from 'api/browser/localstorage/get'; import setLocalStorageApi from 'api/browser/localstorage/set'; import logEvent from 'api/common/logEvent'; +import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences'; import NotFound from 'components/NotFound'; import Spinner from 'components/Spinner'; import { FeatureKeys } from 'constants/features'; @@ -24,12 +25,17 @@ import AlertRuleProvider from 'providers/Alert'; import { DashboardProvider } from 'providers/Dashboard/Dashboard'; import { QueryBuilderProvider } from 'providers/QueryBuilder'; import { Suspense, useEffect, useState } from 'react'; +import { useQuery } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { Route, Router, Switch } from 'react-router-dom'; import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; -import { UPDATE_FEATURE_FLAG_RESPONSE } from 'types/actions/app'; +import { + UPDATE_FEATURE_FLAG_RESPONSE, + UPDATE_IS_FETCHING_ORG_PREFERENCES, + UPDATE_ORG_PREFERENCES, +} from 'types/actions/app'; import AppReducer, { User } from 'types/reducer/app'; import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app'; @@ -65,6 +71,30 @@ function App(): JSX.Element { const isPremiumSupportEnabled = useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false; + const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({ + queryFn: () => getAllOrgPreferences(), + queryKey: ['getOrgPreferences'], + enabled: isLoggedInState, + }); + + useEffect(() => { + if (orgPreferences && !isLoadingOrgPreferences) { + dispatch({ + type: UPDATE_IS_FETCHING_ORG_PREFERENCES, + payload: { + isFetchingOrgPreferences: false, + }, + }); + + dispatch({ + type: UPDATE_ORG_PREFERENCES, + payload: { + orgPreferences: orgPreferences.payload?.data || null, + }, + }); + } + }, [orgPreferences, dispatch, isLoadingOrgPreferences]); + const featureResponse = useGetFeatureFlag((allFlags) => { dispatch({ type: UPDATE_FEATURE_FLAG_RESPONSE, @@ -182,6 +212,16 @@ function App(): JSX.Element { }, [isLoggedInState, isOnBasicPlan, user]); useEffect(() => { + if (pathname === ROUTES.ONBOARDING) { + window.Intercom('update', { + hide_default_launcher: true, + }); + } else { + window.Intercom('update', { + hide_default_launcher: false, + }); + } + trackPageView(pathname); // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathname]); @@ -204,6 +244,7 @@ function App(): JSX.Element { user, licenseData, isPremiumSupportEnabled, + pathname, ]); useEffect(() => { diff --git a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx index ab8f7d4a5c..e0376a6559 100644 --- a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx @@ -4,10 +4,15 @@ import '../OnboardingQuestionaire.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Button, Input, Typography } from 'antd'; import logEvent from 'api/common/logEvent'; +import editOrg from 'api/user/editOrg'; +import { useNotifications } from 'hooks/useNotifications'; import { ArrowRight, CheckCircle, Loader2 } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; +import { Dispatch, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { UPDATE_ORG_NAME } from 'types/actions/app'; import AppReducer from 'types/reducer/app'; export interface OrgData { @@ -25,10 +30,9 @@ export interface OrgDetails { } interface OrgQuestionsProps { - isLoading: boolean; + currentOrgData: OrgData | null; orgDetails: OrgDetails; - setOrgDetails: (details: OrgDetails) => void; - onNext: () => void; + onNext: (details: OrgDetails) => void; } const observabilityTools = { @@ -49,12 +53,15 @@ const o11yFamiliarityOptions: Record = { }; function OrgQuestions({ - isLoading, + currentOrgData, orgDetails, - setOrgDetails, onNext, }: OrgQuestionsProps): JSX.Element { const { user } = useSelector((state) => state.app); + const { notifications } = useNotifications(); + const dispatch = useDispatch>(); + + const { t } = useTranslation(['organizationsettings', 'common']); const [organisationName, setOrganisationName] = useState( orgDetails?.organisationName || '', @@ -77,6 +84,78 @@ function OrgQuestions({ setOrganisationName(orgDetails.organisationName); }, [orgDetails.organisationName]); + const [isLoading, setIsLoading] = useState(false); + + const handleOrgNameUpdate = async (): Promise => { + /* Early bailout if orgData is not set or if the organisation name is not set or if the organisation name is empty or if the organisation name is the same as the one in the orgData */ + if ( + !currentOrgData || + !organisationName || + organisationName === '' || + orgDetails.organisationName === organisationName + ) { + onNext({ + organisationName, + usesObservability, + observabilityTool, + otherTool, + familiarity, + }); + + return; + } + + try { + setIsLoading(true); + const { statusCode, error } = await editOrg({ + isAnonymous: currentOrgData.isAnonymous, + name: organisationName, + orgId: currentOrgData.id, + }); + if (statusCode === 200) { + dispatch({ + type: UPDATE_ORG_NAME, + payload: { + orgId: currentOrgData?.id, + name: orgDetails.organisationName, + }, + }); + + logEvent('User Onboarding: Org Name Updated', { + organisationName: orgDetails.organisationName, + }); + + onNext({ + organisationName, + usesObservability, + observabilityTool, + otherTool, + familiarity, + }); + } else { + logEvent('User Onboarding: Org Name Update Failed', { + organisationName: orgDetails.organisationName, + }); + + notifications.error({ + message: + error || + t('something_went_wrong', { + ns: 'common', + }), + }); + } + setIsLoading(false); + } catch (error) { + setIsLoading(false); + notifications.error({ + message: t('something_went_wrong', { + ns: 'common', + }), + }); + } + }; + const isValidUsesObservability = (): boolean => { if (usesObservability === null) { return false; @@ -112,23 +191,7 @@ function OrgQuestions({ ]); const handleOnNext = (): void => { - setOrgDetails({ - organisationName, - usesObservability, - observabilityTool, - otherTool, - familiarity, - }); - - logEvent('User Onboarding: Org Questions Answered', { - organisationName, - usesObservability, - observabilityTool, - otherTool, - familiarity, - }); - - onNext(); + handleOrgNameUpdate(); }; return ( diff --git a/frontend/src/container/OnboardingQuestionaire/index.tsx b/frontend/src/container/OnboardingQuestionaire/index.tsx index 97ebc33269..1917cd7a55 100644 --- a/frontend/src/container/OnboardingQuestionaire/index.tsx +++ b/frontend/src/container/OnboardingQuestionaire/index.tsx @@ -4,9 +4,9 @@ import { Skeleton } from 'antd'; import { NotificationInstance } from 'antd/es/notification/interface'; import logEvent from 'api/common/logEvent'; import updateProfileAPI from 'api/onboarding/updateProfile'; +import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences'; import getOrgPreference from 'api/preferences/getOrgPreference'; import updateOrgPreferenceAPI from 'api/preferences/updateOrgPreference'; -import editOrg from 'api/user/editOrg'; import getOrgUser from 'api/user/getOrgUser'; import { AxiosError } from 'axios'; import { SOMETHING_WENT_WRONG } from 'constants/api'; @@ -16,12 +16,14 @@ import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; import { isEmpty } from 'lodash-es'; import { Dispatch, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; -import { UPDATE_ORG_NAME } from 'types/actions/app'; +import { + UPDATE_IS_FETCHING_ORG_PREFERENCES, + UPDATE_ORG_PREFERENCES, +} from 'types/actions/app'; import AppReducer from 'types/reducer/app'; import { @@ -67,8 +69,8 @@ const INITIAL_OPTIMISE_SIGNOZ_DETAILS: OptimiseSignozDetails = { function OnboardingQuestionaire(): JSX.Element { const { notifications } = useNotifications(); - - const [currentStep, setCurrentStep] = useState(4); + const { org } = useSelector((state) => state.app); + const [currentStep, setCurrentStep] = useState(1); const [orgDetails, setOrgDetails] = useState(INITIAL_ORG_DETAILS); const [signozDetails, setSignozDetails] = useState( INITIAL_SIGNOZ_DETAILS, @@ -81,9 +83,6 @@ function OnboardingQuestionaire(): JSX.Element { InviteTeamMembersProps[] | null >(null); - const { t } = useTranslation(['organizationsettings', 'common']); - const { org } = useSelector((state) => state.app); - const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({ queryFn: () => getOrgUser({ @@ -93,26 +92,57 @@ function OnboardingQuestionaire(): JSX.Element { }); const dispatch = useDispatch>(); - const [orgData, setOrgData] = useState(null); + const [currentOrgData, setCurrentOrgData] = useState(null); const [isOnboardingComplete, setIsOnboardingComplete] = useState( false, ); - const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({ + const { + data: onboardingPreferenceData, + isLoading: isLoadingOnboardingPreference, + } = useQuery({ queryFn: () => getOrgPreference({ preferenceID: 'ORG_ONBOARDING' }), queryKey: ['getOrgPreferences', 'ORG_ONBOARDING'], }); + const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({ + queryFn: () => getAllOrgPreferences(), + queryKey: ['getOrgPreferences'], + enabled: isOnboardingComplete, + }); + useEffect(() => { - if (!isLoadingOrgPreferences && !isEmpty(orgPreferences?.payload?.data)) { - const preferenceId = orgPreferences?.payload?.data?.preference_id; - const preferenceValue = orgPreferences?.payload?.data?.preference_value; + if (orgPreferences && !isLoadingOrgPreferences) { + dispatch({ + type: UPDATE_IS_FETCHING_ORG_PREFERENCES, + payload: { + isFetchingOrgPreferences: false, + }, + }); + + dispatch({ + type: UPDATE_ORG_PREFERENCES, + payload: { + orgPreferences: orgPreferences.payload?.data || null, + }, + }); + } + }, [orgPreferences, dispatch, isLoadingOrgPreferences]); + + useEffect(() => { + if ( + !isLoadingOnboardingPreference && + !isEmpty(onboardingPreferenceData?.payload?.data) + ) { + const preferenceId = onboardingPreferenceData?.payload?.data?.preference_id; + const preferenceValue = + onboardingPreferenceData?.payload?.data?.preference_value; if (preferenceId === 'ORG_ONBOARDING') { setIsOnboardingComplete(preferenceValue as boolean); } } - }, [orgPreferences, isLoadingOrgPreferences]); + }, [onboardingPreferenceData, isLoadingOnboardingPreference]); const checkFirstTimeUser = (): boolean => { const users = orgUsers?.payload || []; @@ -126,7 +156,7 @@ function OnboardingQuestionaire(): JSX.Element { useEffect(() => { // Only run this effect if the org users and preferences are loaded - if (!isLoadingOrgUsers && !isLoadingOrgPreferences) { + if (!isLoadingOrgUsers && !isLoadingOnboardingPreference) { const isFirstUser = checkFirstTimeUser(); // Redirect to get started if it's not the first user or if the onboarding is complete @@ -144,14 +174,14 @@ function OnboardingQuestionaire(): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [ isLoadingOrgUsers, - isLoadingOrgPreferences, + isLoadingOnboardingPreference, isOnboardingComplete, orgUsers, ]); useEffect(() => { if (org) { - setOrgData(org[0]); + setCurrentOrgData(org[0]); setOrgDetails({ ...orgDetails, @@ -166,70 +196,6 @@ function OnboardingQuestionaire(): JSX.Element { optimiseSignozDetails.hostsPerDay === 0 && optimiseSignozDetails.services === 0; - const [isLoading, setIsLoading] = useState(false); - - const handleOrgNameUpdate = async (): Promise => { - /* Early bailout if orgData is not set or if the organisation name is not set or if the organisation name is empty or if the organisation name is the same as the one in the orgData */ - if ( - !orgData || - !orgDetails.organisationName || - orgDetails.organisationName === '' || - orgData.name === orgDetails.organisationName - ) { - setCurrentStep(2); - - return; - } - - try { - setIsLoading(true); - const { statusCode, error } = await editOrg({ - isAnonymous: orgData?.isAnonymous, - name: orgDetails.organisationName, - orgId: orgData?.id, - }); - if (statusCode === 200) { - dispatch({ - type: UPDATE_ORG_NAME, - payload: { - orgId: orgData?.id, - name: orgDetails.organisationName, - }, - }); - - logEvent('User Onboarding: Org Name Updated', { - organisationName: orgDetails.organisationName, - }); - - setCurrentStep(2); - } else { - logEvent('User Onboarding: Org Name Update Failed', { - organisationName: orgDetails.organisationName, - }); - - notifications.error({ - message: - error || - t('something_went_wrong', { - ns: 'common', - }), - }); - } - setIsLoading(false); - } catch (error) { - setIsLoading(false); - notifications.error({ - message: t('something_went_wrong', { - ns: 'common', - }), - }); - } - }; - - const handleOrgDetailsUpdate = (): void => { - handleOrgNameUpdate(); - }; - const { mutate: updateProfile, isLoading: isUpdatingProfile } = useMutation( updateProfileAPI, { @@ -289,20 +255,22 @@ function OnboardingQuestionaire(): JSX.Element {
- {(isLoadingOrgPreferences || isLoadingOrgUsers) && ( + {(isLoadingOnboardingPreference || isLoadingOrgUsers) && (
)} - {!isLoadingOrgPreferences && !isLoadingOrgUsers && ( + {!isLoadingOnboardingPreference && !isLoadingOrgUsers && ( <> {currentStep === 1 && ( { + setOrgDetails(orgDetails); + setCurrentStep(2); + }} /> )} diff --git a/frontend/src/store/reducers/app.ts b/frontend/src/store/reducers/app.ts index bdb80d2565..3ae5df9699 100644 --- a/frontend/src/store/reducers/app.ts +++ b/frontend/src/store/reducers/app.ts @@ -8,10 +8,12 @@ import { UPDATE_CURRENT_ERROR, UPDATE_CURRENT_VERSION, UPDATE_FEATURE_FLAG_RESPONSE, + UPDATE_IS_FETCHING_ORG_PREFERENCES, UPDATE_LATEST_VERSION, UPDATE_LATEST_VERSION_ERROR, UPDATE_ORG, UPDATE_ORG_NAME, + UPDATE_ORG_PREFERENCES, UPDATE_USER, UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN, UPDATE_USER_FLAG, @@ -59,6 +61,8 @@ const InitialValue: InitialValueTypes = { userFlags: {}, ee: 'Y', setupCompleted: true, + orgPreferences: null, + isFetchingOrgPreferences: true, }; const appReducer = ( @@ -73,6 +77,17 @@ const appReducer = ( }; } + case UPDATE_ORG_PREFERENCES: { + return { ...state, orgPreferences: action.payload.orgPreferences }; + } + + case UPDATE_IS_FETCHING_ORG_PREFERENCES: { + return { + ...state, + isFetchingOrgPreferences: action.payload.isFetchingOrgPreferences, + }; + } + case UPDATE_FEATURE_FLAG_RESPONSE: { return { ...state, diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 130464a980..18db88688f 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -276,26 +276,39 @@ notifications - 2050 } @font-face { - font-family: 'Inter'; - src: url('../public/fonts/Inter-VariableFont_opsz,wght.ttf') format('truetype'); - font-weight: 300 700; - font-style: normal; + font-family: 'Inter'; + src: url('../public/fonts/Inter-VariableFont_opsz,wght.ttf') format('truetype'); + font-weight: 300 700; + font-style: normal; } @font-face { - font-family: 'Work Sans'; - src: url('../public/fonts/WorkSans-VariableFont_wght.ttf') format('truetype'); - font-weight: 500; - font-style: normal; + font-family: 'Work Sans'; + src: url('../public/fonts/WorkSans-VariableFont_wght.ttf') format('truetype'); + font-weight: 500; + font-style: normal; } @font-face { - font-family: 'Space Mono'; - src: url('../public/fonts/SpaceMono-Regular.ttf') format('truetype'); - font-weight: normal; - font-style: normal; + font-family: 'Space Mono'; + src: url('../public/fonts/SpaceMono-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; } @font-face { - font-family: 'Fira Code'; - src: url('../public/fonts/FiraCode-VariableFont_wght.ttf') format('truetype'); - font-weight: 300 700; - font-style: normal; + font-family: 'Fira Code'; + src: url('../public/fonts/FiraCode-VariableFont_wght.ttf') format('truetype'); + font-weight: 300 700; + font-style: normal; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.animate-spin { + animation: spin 1s linear infinite; } diff --git a/frontend/src/types/actions/app.ts b/frontend/src/types/actions/app.ts index 54b1992af2..93a9dac5df 100644 --- a/frontend/src/types/actions/app.ts +++ b/frontend/src/types/actions/app.ts @@ -25,7 +25,10 @@ export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME'; export const UPDATE_ORG = 'UPDATE_ORG'; export const UPDATE_CONFIGS = 'UPDATE_CONFIGS'; export const UPDATE_USER_FLAG = 'UPDATE_USER_FLAG'; +export const UPDATE_ORG_PREFERENCES = 'UPDATE_ORG_PREFERENCES'; export const UPDATE_FEATURE_FLAG_RESPONSE = 'UPDATE_FEATURE_FLAG_RESPONSE'; +export const UPDATE_IS_FETCHING_ORG_PREFERENCES = + 'UPDATE_IS_FETCHING_ORG_PREFERENCES'; export interface LoggedInUser { type: typeof LOGGED_IN; @@ -130,6 +133,20 @@ export interface UpdateFeatureFlag { }; } +export interface UpdateOrgPreferences { + type: typeof UPDATE_ORG_PREFERENCES; + payload: { + orgPreferences: AppReducer['orgPreferences']; + }; +} + +export interface UpdateIsFetchingOrgPreferences { + type: typeof UPDATE_IS_FETCHING_ORG_PREFERENCES; + payload: { + isFetchingOrgPreferences: AppReducer['isFetchingOrgPreferences']; + }; +} + export type AppAction = | LoggedInUser | UpdateAppVersion @@ -143,4 +160,6 @@ export type AppAction = | UpdateOrg | UpdateConfigs | UpdateUserFlag - | UpdateFeatureFlag; + | UpdateFeatureFlag + | UpdateOrgPreferences + | UpdateIsFetchingOrgPreferences; diff --git a/frontend/src/types/api/preferences/userOrgPreferences.ts b/frontend/src/types/api/preferences/userOrgPreferences.ts index 2c77090769..ab9292596f 100644 --- a/frontend/src/types/api/preferences/userOrgPreferences.ts +++ b/frontend/src/types/api/preferences/userOrgPreferences.ts @@ -1,3 +1,5 @@ +import { OrgPreference } from 'types/reducer/app'; + export interface GetOrgPreferenceResponseProps { status: string; data: Record; @@ -10,7 +12,7 @@ export interface GetUserPreferenceResponseProps { export interface GetAllOrgPreferencesResponseProps { status: string; - data: Record; + data: OrgPreference[]; } export interface GetAllUserPreferencesResponseProps { diff --git a/frontend/src/types/reducer/app.ts b/frontend/src/types/reducer/app.ts index c51defcfb0..cfc431ab03 100644 --- a/frontend/src/types/reducer/app.ts +++ b/frontend/src/types/reducer/app.ts @@ -15,6 +15,18 @@ export interface User { profilePictureURL: UserPayload['profilePictureURL']; } +export interface OrgPreference { + key: string; + name: string; + description: string; + valueType: string; + defaultValue: boolean; + allowedValues: any[]; + isDiscreteValues: boolean; + allowedScopes: string[]; + value: boolean; +} + export default interface AppReducer { isLoggedIn: boolean; currentVersion: string; @@ -30,6 +42,8 @@ export default interface AppReducer { userFlags: null | UserFlags; ee: 'Y' | 'N'; setupCompleted: boolean; + orgPreferences: OrgPreference[] | null; + isFetchingOrgPreferences: boolean; featureResponse: { data: FeatureFlagPayload[] | null; refetch: QueryObserverBaseResult['refetch']; diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index e58843232a..bd27d04934 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -86,7 +86,7 @@ export const routePermission: Record = { LOGS_PIPELINES: ['ADMIN', 'EDITOR', 'VIEWER'], TRACE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED: ['ADMIN', 'EDITOR', 'VIEWER'], - ONBOARDING: ['ADMIN', 'EDITOR', 'VIEWER'], + ONBOARDING: ['ADMIN'], GET_STARTED_APPLICATION_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED_INFRASTRUCTURE_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED_LOGS_MANAGEMENT: ['ADMIN', 'EDITOR', 'VIEWER'],