feat: sidebar revamp (#8087)

* feat: sidebar revamp - initial commit

* feat: move billinga and other isolated routes to settings

* feat: handle channel related routes

* feat: update account settings page

* feat: show dropdown for secondary items

* feat: handle reordering of pinned nav items

* feat: improve font load performance

* feat: update font reference

* feat: update page content styles

* feat: handle external links in sidebar

* feat: handle secondary nav item clicks

* feat: handle pinned nav items reordering

* feat: handle sidenav pinned state using preference, handle light mode

* feat: show sidenav items conditionally

* feat: show version diff indicator only to self hosted users

* feat: show billing to admins only and integrations to cloud and enterprise users

* feat: update fallback link

* feat: handle settings menu items

* fix: settings page reload on nav chnage

* feat: intercom to pylon

* feat: show invite user to admin only

* feat: handle review comments

* chore: remove react query dev tools

* feat: minor ui updates

* feat: update changes based on preference store changes

* feat: handle sidenav shortcut state

* feat: handle scroll for more

* feat: maintain shortcuts order

* feat: manage license ui updates

* feat: manage settings options based on license and roles

* feat: update types

* chore: add logEvents

* feat: update types

* chore: fix type errors

* chore: remove unused variable

* feat: update my settings page test cases

---------

Co-authored-by: makeavish <makeavish786@gmail.com>
This commit is contained in:
Yunus M 2025-06-12 19:55:32 +05:30 committed by GitHub
parent fff7f8fc76
commit c477e0ef16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
80 changed files with 4243 additions and 1067 deletions

View File

@ -9,8 +9,8 @@
"tooltip_notification_channels": "More details on how to setting notification channels", "tooltip_notification_channels": "More details on how to setting notification channels",
"sending_channels_note": "The alerts will be sent to all the configured channels.", "sending_channels_note": "The alerts will be sent to all the configured channels.",
"loading_channels_message": "Loading Channels..", "loading_channels_message": "Loading Channels..",
"page_title_create": "New Notification Channels", "page_title_create": "New Notification Channel",
"page_title_edit": "Edit Notification Channels", "page_title_edit": "Edit Notification Channel",
"button_save_channel": "Save", "button_save_channel": "Save",
"button_test_channel": "Test", "button_test_channel": "Test",
"button_return": "Back", "button_return": "Back",

View File

@ -9,8 +9,8 @@
"tooltip_notification_channels": "More details on how to setting notification channels", "tooltip_notification_channels": "More details on how to setting notification channels",
"sending_channels_note": "The alerts will be sent to all the configured channels.", "sending_channels_note": "The alerts will be sent to all the configured channels.",
"loading_channels_message": "Loading Channels..", "loading_channels_message": "Loading Channels..",
"page_title_create": "New Notification Channels", "page_title_create": "New Notification Channel",
"page_title_edit": "Edit Notification Channels", "page_title_edit": "Edit Notification Channel",
"button_save_channel": "Save", "button_save_channel": "Save",
"button_test_channel": "Test", "button_test_channel": "Test",
"button_return": "Back", "button_return": "Back",

View File

@ -3,6 +3,7 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
import getAll from 'api/v1/user/get'; import getAll from 'api/v1/user/get';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense'; import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history'; import history from 'lib/history';
@ -95,7 +96,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
usersData.data usersData.data
) { ) {
const isOnboardingComplete = orgPreferences?.find( const isOnboardingComplete = orgPreferences?.find(
(preference: Record<string, any>) => preference.name === 'org_onboarding', (preference: Record<string, any>) =>
preference.key === ORG_PREFERENCES.ORG_ONBOARDING,
)?.value; )?.value;
const isFirstUser = checkFirstTimeUser(); const isFirstUser = checkFirstTimeUser();

View File

@ -193,11 +193,12 @@ function App(): JSX.Element {
updatedRoutes = updatedRoutes.filter( updatedRoutes = updatedRoutes.filter(
(route) => route?.path !== ROUTES.BILLING, (route) => route?.path !== ROUTES.BILLING,
); );
}
if (isEnterpriseSelfHostedUser) { if (isEnterpriseSelfHostedUser) {
updatedRoutes.push(LIST_LICENSES); updatedRoutes.push(LIST_LICENSES);
} }
}
// always add support route for cloud users // always add support route for cloud users
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE]; updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
} else { } else {

View File

@ -128,12 +128,11 @@ export const AlertOverview = Loadable(
); );
export const CreateAlertChannelAlerts = Loadable( export const CreateAlertChannelAlerts = Loadable(
() => () => import(/* webpackChunkName: "Create Channels" */ 'pages/Settings'),
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
); );
export const EditAlertChannelsAlerts = Loadable( export const EditAlertChannelsAlerts = Loadable(
() => import(/* webpackChunkName: "Edit Channels" */ 'pages/ChannelsEdit'), () => import(/* webpackChunkName: "Edit Channels" */ 'pages/Settings'),
); );
export const AllAlertChannels = Loadable( export const AllAlertChannels = Loadable(
@ -165,7 +164,7 @@ export const APIKeys = Loadable(
); );
export const MySettings = Loadable( export const MySettings = Loadable(
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'), () => import(/* webpackChunkName: "All MySettings" */ 'pages/Settings'),
); );
export const CustomDomainSettings = Loadable( export const CustomDomainSettings = Loadable(
@ -222,7 +221,7 @@ export const LogsIndexToFields = Loadable(
); );
export const BillingPage = Loadable( export const BillingPage = Loadable(
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Billing'), () => import(/* webpackChunkName: "BillingPage" */ 'pages/Settings'),
); );
export const SupportPage = Loadable( export const SupportPage = Loadable(
@ -249,7 +248,7 @@ export const WorkspaceAccessRestricted = Loadable(
); );
export const ShortcutsPage = Loadable( export const ShortcutsPage = Loadable(
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'), () => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Settings'),
); );
export const InstalledIntegrations = Loadable( export const InstalledIntegrations = Loadable(

View File

@ -7,12 +7,9 @@ import {
AlertOverview, AlertOverview,
AllAlertChannels, AllAlertChannels,
AllErrors, AllErrors,
APIKeys,
ApiMonitoring, ApiMonitoring,
BillingPage,
CreateAlertChannelAlerts, CreateAlertChannelAlerts,
CreateNewAlerts, CreateNewAlerts,
CustomDomainSettings,
DashboardPage, DashboardPage,
DashboardWidget, DashboardWidget,
EditAlertChannelsAlerts, EditAlertChannelsAlerts,
@ -20,7 +17,6 @@ import {
ErrorDetails, ErrorDetails,
Home, Home,
InfrastructureMonitoring, InfrastructureMonitoring,
IngestionSettings,
InstalledIntegrations, InstalledIntegrations,
LicensePage, LicensePage,
ListAllALertsPage, ListAllALertsPage,
@ -31,12 +27,10 @@ import {
LogsIndexToFields, LogsIndexToFields,
LogsSaveViews, LogsSaveViews,
MetricsExplorer, MetricsExplorer,
MySettings,
NewDashboardPage, NewDashboardPage,
OldLogsExplorer, OldLogsExplorer,
Onboarding, Onboarding,
OnboardingV2, OnboardingV2,
OrganizationSettings,
OrgOnboarding, OrgOnboarding,
PasswordReset, PasswordReset,
PipelinePage, PipelinePage,
@ -45,7 +39,6 @@ import {
ServicesTablePage, ServicesTablePage,
ServiceTopLevelOperationsPage, ServiceTopLevelOperationsPage,
SettingsPage, SettingsPage,
ShortcutsPage,
SignupPage, SignupPage,
SomethingWentWrong, SomethingWentWrong,
StatusPage, StatusPage,
@ -150,7 +143,7 @@ const routes: AppRoutes[] = [
}, },
{ {
path: ROUTES.SETTINGS, path: ROUTES.SETTINGS,
exact: true, exact: false,
component: SettingsPage, component: SettingsPage,
isPrivate: true, isPrivate: true,
key: 'SETTINGS', key: 'SETTINGS',
@ -295,41 +288,6 @@ const routes: AppRoutes[] = [
isPrivate: true, isPrivate: true,
key: 'VERSION', key: 'VERSION',
}, },
{
path: ROUTES.ORG_SETTINGS,
exact: true,
component: OrganizationSettings,
isPrivate: true,
key: 'ORG_SETTINGS',
},
{
path: ROUTES.INGESTION_SETTINGS,
exact: true,
component: IngestionSettings,
isPrivate: true,
key: 'INGESTION_SETTINGS',
},
{
path: ROUTES.API_KEYS,
exact: true,
component: APIKeys,
isPrivate: true,
key: 'API_KEYS',
},
{
path: ROUTES.MY_SETTINGS,
exact: true,
component: MySettings,
isPrivate: true,
key: 'MY_SETTINGS',
},
{
path: ROUTES.CUSTOM_DOMAIN_SETTINGS,
exact: true,
component: CustomDomainSettings,
isPrivate: true,
key: 'CUSTOM_DOMAIN_SETTINGS',
},
{ {
path: ROUTES.LOGS, path: ROUTES.LOGS,
exact: true, exact: true,
@ -393,13 +351,6 @@ const routes: AppRoutes[] = [
key: 'SOMETHING_WENT_WRONG', key: 'SOMETHING_WENT_WRONG',
isPrivate: false, isPrivate: false,
}, },
{
path: ROUTES.BILLING,
exact: true,
component: BillingPage,
key: 'BILLING',
isPrivate: true,
},
{ {
path: ROUTES.WORKSPACE_LOCKED, path: ROUTES.WORKSPACE_LOCKED,
exact: true, exact: true,
@ -421,13 +372,6 @@ const routes: AppRoutes[] = [
isPrivate: true, isPrivate: true,
key: 'WORKSPACE_ACCESS_RESTRICTED', key: 'WORKSPACE_ACCESS_RESTRICTED',
}, },
{
path: ROUTES.SHORTCUTS,
exact: true,
component: ShortcutsPage,
isPrivate: true,
key: 'SHORTCUTS',
},
{ {
path: ROUTES.INTEGRATIONS, path: ROUTES.INTEGRATIONS,
exact: true, exact: true,

View File

@ -1,7 +1,12 @@
import { Tabs, TabsProps } from 'antd'; import { Tabs, TabsProps } from 'antd';
import { useLocation, useParams } from 'react-router-dom';
import { RouteTabProps } from './types'; import { RouteTabProps } from './types';
interface Params {
[key: string]: string;
}
function RouteTab({ function RouteTab({
routes, routes,
activeKey, activeKey,
@ -9,19 +14,38 @@ function RouteTab({
history, history,
...rest ...rest
}: RouteTabProps & TabsProps): JSX.Element { }: RouteTabProps & TabsProps): JSX.Element {
const params = useParams<Params>();
const location = useLocation();
// Replace dynamic parameters in routes
const routesWithParams = routes.map((route) => ({
...route,
route: route.route.replace(
/:(\w+)/g,
(match, param) => params[param] || match,
),
}));
// Find the matching route for the current pathname
const currentRoute = routesWithParams.find((route) => {
const routePattern = route.route.replace(/:(\w+)/g, '([^/]+)');
const regex = new RegExp(`^${routePattern}$`);
return regex.test(location.pathname);
});
const onChange = (activeRoute: string): void => { const onChange = (activeRoute: string): void => {
if (onChangeHandler) { if (onChangeHandler) {
onChangeHandler(activeRoute); onChangeHandler(activeRoute);
} }
const selectedRoute = routes.find((e) => e.key === activeRoute); const selectedRoute = routesWithParams.find((e) => e.key === activeRoute);
if (selectedRoute) { if (selectedRoute) {
history.push(selectedRoute.route); history.push(selectedRoute.route);
} }
}; };
const items = routes.map(({ Component, name, route, key }) => ({ const items = routesWithParams.map(({ Component, name, route, key }) => ({
label: name, label: name,
key, key,
tabKey: route, tabKey: route,
@ -32,8 +56,8 @@ function RouteTab({
<Tabs <Tabs
onChange={onChange} onChange={onChange}
destroyInactiveTabPane destroyInactiveTabPane
activeKey={activeKey} activeKey={currentRoute?.key || activeKey}
defaultActiveKey={activeKey} defaultActiveKey={currentRoute?.key || activeKey}
animated animated
items={items} items={items}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading

View File

@ -0,0 +1,18 @@
export const ORG_PREFERENCES = {
ORG_ONBOARDING: 'org_onboarding',
WELCOME_CHECKLIST_DO_LATER: 'welcome_checklist_do_later',
WELCOME_CHECKLIST_SEND_LOGS_SKIPPED: 'welcome_checklist_send_logs_skipped',
WELCOME_CHECKLIST_SEND_TRACES_SKIPPED: 'welcome_checklist_send_traces_skipped',
WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED:
'welcome_checklist_setup_alerts_skipped',
WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED:
'welcome_checklist_setup_saved_view_skipped',
WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED:
'welcome_checklist_send_infra_metrics_skipped',
WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED:
'welcome_checklist_setup_dashboards_skipped',
WELCOME_CHECKLIST_SETUP_WORKSPACE_SKIPPED:
'welcome_checklist_setup_workspace_skipped',
WELCOME_CHECKLIST_ADD_DATA_SOURCE_SKIPPED:
'welcome_checklist_add_data_source_skipped',
};

View File

@ -29,12 +29,12 @@ const ROUTES = {
ALERT_OVERVIEW: '/alerts/overview', ALERT_OVERVIEW: '/alerts/overview',
ALL_CHANNELS: '/settings/channels', ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/settings/channels/new', CHANNELS_NEW: '/settings/channels/new',
CHANNELS_EDIT: '/settings/channels/:id', CHANNELS_EDIT: '/settings/channels/edit/:id',
ALL_ERROR: '/exceptions', ALL_ERROR: '/exceptions',
ERROR_DETAIL: '/error-detail', ERROR_DETAIL: '/error-detail',
VERSION: '/status', VERSION: '/status',
MY_SETTINGS: '/my-settings',
SETTINGS: '/settings', SETTINGS: '/settings',
MY_SETTINGS: '/settings/my-settings',
ORG_SETTINGS: '/settings/org-settings', ORG_SETTINGS: '/settings/org-settings',
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings', CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
API_KEYS: '/settings/api-keys', API_KEYS: '/settings/api-keys',
@ -52,7 +52,7 @@ const ROUTES = {
LIST_LICENSES: '/licenses', LIST_LICENSES: '/licenses',
LOGS_INDEX_FIELDS: '/logs-explorer/index-fields', LOGS_INDEX_FIELDS: '/logs-explorer/index-fields',
TRACE_EXPLORER: '/trace-explorer', TRACE_EXPLORER: '/trace-explorer',
BILLING: '/billing', BILLING: '/settings/billing',
SUPPORT: '/support', SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs/saved-views', LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views', TRACES_SAVE_VIEWS: '/traces/saved-views',
@ -60,7 +60,7 @@ const ROUTES = {
TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId', TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId',
WORKSPACE_LOCKED: '/workspace-locked', WORKSPACE_LOCKED: '/workspace-locked',
WORKSPACE_SUSPENDED: '/workspace-suspended', WORKSPACE_SUSPENDED: '/workspace-suspended',
SHORTCUTS: '/shortcuts', SHORTCUTS: '/settings/shortcuts',
INTEGRATIONS: '/integrations', INTEGRATIONS: '/integrations',
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka', MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail', MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',

View File

@ -0,0 +1,4 @@
export const USER_PREFERENCES = {
SIDENAV_PINNED: 'sidenav_pinned',
NAV_SHORTCUTS: 'nav_shortcuts',
};

View File

@ -21,7 +21,7 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
const [action] = useComponentPermission(['new_alert_action'], user.role); const [action] = useComponentPermission(['new_alert_action'], user.role);
const onClickEditHandler = useCallback((id: string) => { const onClickEditHandler = useCallback((id: string) => {
history.replace( history.push(
generatePath(ROUTES.CHANNELS_EDIT, { generatePath(ROUTES.CHANNELS_EDIT, {
id, id,
}), }),

View File

@ -0,0 +1,4 @@
.alert-channels-container {
width: 90%;
margin: 12px auto;
}

View File

@ -1,3 +1,5 @@
import './AllAlertChannels.styles.scss';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { Tooltip, Typography } from 'antd'; import { Tooltip, Typography } from 'antd';
import getAll from 'api/channels/getAll'; import getAll from 'api/channels/getAll';
@ -56,7 +58,7 @@ function AlertChannels(): JSX.Element {
} }
return ( return (
<> <div className="alert-channels-container">
<ButtonContainer> <ButtonContainer>
<Paragraph ellipsis type="secondary"> <Paragraph ellipsis type="secondary">
{t('sending_channels_note')} {t('sending_channels_note')}
@ -87,7 +89,7 @@ function AlertChannels(): JSX.Element {
</ButtonContainer> </ButtonContainer>
<AlertChannelsComponent allChannels={data?.data || []} /> <AlertChannelsComponent allChannels={data?.data || []} />
</> </div>
); );
} }

View File

@ -22,6 +22,12 @@
width: 100%; width: 100%;
} }
} }
&.side-nav-pinned {
.app-content {
width: calc(100% - 240px);
}
}
} }
.chat-support-gateway { .chat-support-gateway {

View File

@ -18,6 +18,7 @@ import { Events } from 'constants/events';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { USER_PREFERENCES } from 'constants/userPreferences';
import SideNav from 'container/SideNav'; import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav'; import TopNav from 'container/TopNav';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -27,7 +28,6 @@ import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
import { isNull } from 'lodash-es'; import { isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
import { useAppContext } from 'providers/App/App'; import { useAppContext } from 'providers/App/App';
import { import {
ReactNode, ReactNode,
@ -41,7 +41,7 @@ import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useMutation, useQueries } from 'react-query'; import { useMutation, useQueries } from 'react-query';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { matchPath, useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { import {
@ -80,6 +80,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
featureFlags, featureFlags,
isFetchingFeatureFlags, isFetchingFeatureFlags,
featureFlagsFetchError, featureFlagsFetchError,
userPreferences,
} = useAppContext(); } = useAppContext();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
@ -330,53 +331,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}); });
}, [manageCreditCard]); }, [manageCreditCard]);
const isHome = (): boolean => routeKey === 'HOME';
const isLogsView = (): boolean =>
routeKey === 'LOGS' ||
routeKey === 'LOGS_EXPLORER' ||
routeKey === 'LOGS_PIPELINES' ||
routeKey === 'LOGS_SAVE_VIEWS';
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
const isExceptionsView = (): boolean => routeKey === 'ALL_ERROR';
const isTracesView = (): boolean =>
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
const isMessagingQueues = (): boolean =>
routeKey === 'MESSAGING_QUEUES_KAFKA' ||
routeKey === 'MESSAGING_QUEUES_KAFKA_DETAIL' ||
routeKey === 'MESSAGING_QUEUES_CELERY_TASK' ||
routeKey === 'MESSAGING_QUEUES_OVERVIEW';
const isCloudIntegrationPage = (): boolean =>
routeKey === 'INTEGRATIONS' &&
new URLSearchParams(window.location.search).get('integration') ===
INTEGRATION_TYPES.AWS_INTEGRATION;
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
const isInfraMonitoring = (): boolean =>
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS' ||
routeKey === 'INFRASTRUCTURE_MONITORING_KUBERNETES';
const isTracesFunnels = (): boolean => routeKey === 'TRACES_FUNNELS';
const isTracesFunnelDetails = (): boolean =>
!!matchPath(pathname, ROUTES.TRACES_FUNNELS_DETAIL);
const isPathMatch = (regex: RegExp): boolean => regex.test(pathname);
const isDashboardView = (): boolean =>
isPathMatch(/^\/dashboard\/[a-zA-Z0-9_-]+$/);
const isDashboardWidgetView = (): boolean =>
isPathMatch(/^\/dashboard\/[a-zA-Z0-9_-]+\/new$/);
const isTraceDetailsView = (): boolean =>
isPathMatch(/^\/trace\/[a-zA-Z0-9]+(\?.*)?$/);
useEffect(() => { useEffect(() => {
if (isDarkMode) { if (isDarkMode) {
document.body.classList.remove('lightMode'); document.body.classList.remove('lightMode');
@ -593,6 +547,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div> </div>
); );
const sideNavPinned = userPreferences?.find(
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
)?.value as boolean;
return ( return (
<Layout className={cx(isDarkMode ? 'darkMode dark' : 'lightMode')}> <Layout className={cx(isDarkMode ? 'darkMode dark' : 'lightMode')}>
<Helmet> <Helmet>
@ -645,9 +603,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)} )}
<Flex <Flex
className={cx('app-layout', isDarkMode ? 'darkMode dark' : 'lightMode')} className={cx(
'app-layout',
isDarkMode ? 'darkMode dark' : 'lightMode',
sideNavPinned ? 'side-nav-pinned' : '',
)}
> >
{isToDisplayLayout && !renderFullScreen && <SideNav />} {isToDisplayLayout && !renderFullScreen && (
<SideNav isPinned={sideNavPinned} />
)}
<div <div
className={cx('app-content', { className={cx('app-content', {
'full-screen-content': renderFullScreen, 'full-screen-content': renderFullScreen,
@ -657,32 +621,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}> <Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent data-overlayscrollbars-initialize> <LayoutContent data-overlayscrollbars-initialize>
<OverlayScrollbar> <OverlayScrollbar>
<ChildrenContainer <ChildrenContainer>
style={{
margin:
isHome() ||
isLogsView() ||
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView() ||
isAlertHistory() ||
isAlertOverview() ||
isMessagingQueues() ||
isCloudIntegrationPage() ||
isInfraMonitoring() ||
isApiMonitoringView() ||
isExceptionsView()
? 0
: '0 1rem',
...(isTraceDetailsView() ||
isTracesFunnels() ||
isTracesFunnelDetails()
? { margin: 0 }
: {}),
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />} {isToDisplayLayout && !renderFullScreen && <TopNav />}
{children} {children}
</ChildrenContainer> </ChildrenContainer>

View File

@ -1,7 +1,8 @@
.billing-container { .billing-container {
margin-bottom: 40px; margin-bottom: 40px;
padding-top: 36px; padding-top: 36px;
width: 65%; width: 90%;
margin: 0 auto;
.billing-summary { .billing-summary {
margin: 24px 8px; margin: 24px 8px;

View File

@ -0,0 +1,15 @@
.create-alert-channels-container {
width: 90%;
margin: 12px auto;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
padding: 16px;
.form-alert-channels-title {
margin-top: 0px;
margin-bottom: 16px;
}
}

View File

@ -1,3 +1,5 @@
import './CreateAlertChannels.styles.scss';
import { Form } from 'antd'; import { Form } from 'antd';
import createEmail from 'api/channels/createEmail'; import createEmail from 'api/channels/createEmail';
import createMsTeamsApi from 'api/channels/createMsTeams'; import createMsTeamsApi from 'api/channels/createMsTeams';
@ -477,6 +479,7 @@ function CreateAlertChannels({
); );
return ( return (
<div className="create-alert-channels-container">
<FormAlertChannels <FormAlertChannels
{...{ {...{
formInstance, formInstance,
@ -497,6 +500,7 @@ function CreateAlertChannels({
}, },
}} }}
/> />
</div>
); );
} }

View File

@ -57,7 +57,9 @@ function FormAlertChannels({
return ( return (
<> <>
<Typography.Title level={3}>{title}</Typography.Title> <Typography.Title level={4} className="form-alert-channels-title">
{title}
</Typography.Title>
<Form initialValues={initialValue} layout="vertical" form={formInstance}> <Form initialValues={initialValue} layout="vertical" form={formInstance}>
<Form.Item label={t('field_channel_name')} labelAlign="left" name="name"> <Form.Item label={t('field_channel_name')} labelAlign="left" name="name">

View File

@ -12,6 +12,7 @@ import Header from 'components/Header/Header';
import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
@ -184,18 +185,25 @@ export default function Home(): JSX.Element {
); );
const processUserPreferences = (userPreferences: UserPreference[]): void => { const processUserPreferences = (userPreferences: UserPreference[]): void => {
const checklistSkipped = userPreferences?.find( const checklistSkipped = Boolean(
(preference) => preference.name === 'welcome_checklist_do_later', userPreferences?.find(
)?.value; (preference) =>
preference.name === ORG_PREFERENCES.WELCOME_CHECKLIST_DO_LATER,
)?.value,
);
const updatedChecklistItems = cloneDeep(checklistItems); const updatedChecklistItems = cloneDeep(checklistItems);
const newChecklistItems = updatedChecklistItems.map((item) => { const newChecklistItems = updatedChecklistItems.map((item) => {
const newItem = { ...item }; const newItem = { ...item };
newItem.isSkipped =
const isSkipped = Boolean(
userPreferences?.find( userPreferences?.find(
(preference) => preference.name === item.skippedPreferenceKey, (preference) => preference.name === item.skippedPreferenceKey,
)?.value || false; )?.value,
);
newItem.isSkipped = isSkipped || false;
return newItem; return newItem;
}); });
@ -239,7 +247,7 @@ export default function Home(): JSX.Element {
setUpdatingUserPreferences(true); setUpdatingUserPreferences(true);
updateUserPreference({ updateUserPreference({
name: 'welcome_checklist_do_later', name: ORG_PREFERENCES.WELCOME_CHECKLIST_DO_LATER,
value: true, value: true,
}); });
}; };

View File

@ -1,17 +1,19 @@
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { ChecklistItem } from './HomeChecklist/HomeChecklist'; import { ChecklistItem } from './HomeChecklist/HomeChecklist';
export const checkListStepToPreferenceKeyMap = { export const checkListStepToPreferenceKeyMap = {
WILL_DO_LATER: 'welcome_checklist_do_later', WILL_DO_LATER: ORG_PREFERENCES.WELCOME_CHECKLIST_DO_LATER,
SEND_LOGS: 'welcome_checklist_send_logs_skipped', SEND_LOGS: ORG_PREFERENCES.WELCOME_CHECKLIST_SEND_LOGS_SKIPPED,
SEND_TRACES: 'welcome_checklist_send_traces_skipped', SEND_TRACES: ORG_PREFERENCES.WELCOME_CHECKLIST_SEND_TRACES_SKIPPED,
SEND_INFRA_METRICS: 'welcome_checklist_send_infra_metrics_skipped', SEND_INFRA_METRICS:
SETUP_DASHBOARDS: 'welcome_checklist_setup_dashboards_skipped', ORG_PREFERENCES.WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED,
SETUP_ALERTS: 'welcome_checklist_setup_alerts_skipped', SETUP_DASHBOARDS: ORG_PREFERENCES.WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED,
SETUP_SAVED_VIEWS: 'welcome_checklist_setup_saved_view_skipped', SETUP_ALERTS: ORG_PREFERENCES.WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED,
SETUP_WORKSPACE: 'welcome_checklist_setup_workspace_skipped', SETUP_SAVED_VIEWS: ORG_PREFERENCES.WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED,
ADD_DATA_SOURCE: 'welcome_checklist_add_data_source_skipped', SETUP_WORKSPACE: ORG_PREFERENCES.WELCOME_CHECKLIST_SETUP_WORKSPACE_SKIPPED,
ADD_DATA_SOURCE: ORG_PREFERENCES.WELCOME_CHECKLIST_ADD_DATA_SOURCE_SKIPPED,
}; };
export const DOCS_LINKS = { export const DOCS_LINKS = {

View File

@ -0,0 +1,91 @@
.licenses-page {
max-height: 100vh;
overflow: hidden;
.licenses-page-header {
border-bottom: 1px solid var(--Slate-500, #161922);
background: rgba(11, 12, 14, 0.7);
backdrop-filter: blur(20px);
.licenses-page-header-title {
color: var(--Vanilla-100, #fff);
text-align: center;
font-family: Inter;
font-size: 13px;
font-style: normal;
line-height: 14px;
letter-spacing: 0.4px;
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
}
}
.licenses-page-content-container {
display: flex;
flex-direction: row;
align-items: flex-start;
.licenses-page-content {
flex: 1;
height: calc(100vh - 48px);
background: var(--Ink-500, #0b0c0e);
padding: 10px 8px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
}
}
.lightMode {
.licenses-page {
.licenses-page-header {
border-bottom: 1px solid var(--bg-vanilla-300);
background: #fff;
backdrop-filter: blur(20px);
.licenses-page-header-title {
color: var(--bg-ink-400);
background: var(--bg-vanilla-100);
border-right: 1px solid var(--bg-vanilla-300);
}
}
.licenses-page-content-container {
.licenses-page-content {
background: var(--bg-vanilla-100);
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
}
}
}

View File

@ -1,5 +1,7 @@
import { Tabs } from 'antd'; import './Licenses.styles.scss';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { Wrench } from 'lucide-react';
import { useAppContext } from 'providers/App/App'; import { useAppContext } from 'providers/App/App';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -13,16 +15,19 @@ function Licenses(): JSX.Element {
return <Spinner tip={t('loading_licenses')} height="90vh" />; return <Spinner tip={t('loading_licenses')} height="90vh" />;
} }
const tabs = [
{
label: t('tab_current_license'),
key: 'licenses',
children: <ApplyLicenseForm licenseRefetch={activeLicenseRefetch} />,
},
];
return ( return (
<Tabs destroyInactiveTabPane defaultActiveKey="licenses" items={tabs} /> <div className="licenses-page">
<header className="licenses-page-header">
<div className="licenses-page-header-title">
<Wrench size={16} />
License
</div>
</header>
<div className="licenses-page-content-container">
<ApplyLicenseForm licenseRefetch={activeLicenseRefetch} />
</div>
</div>
); );
} }

View File

@ -3,8 +3,7 @@ import styled from 'styled-components';
export const ApplyFormContainer = styled.div` export const ApplyFormContainer = styled.div`
&&& { &&& {
padding-top: 1em; padding: 16px;
padding-bottom: 1em;
} }
`; `;

View File

@ -1,3 +1,12 @@
.my-settings-container {
display: flex;
flex-direction: column;
gap: 48px;
width: 80%;
margin: 12px auto;
}
.flexBtn { .flexBtn {
display: flex; display: flex;
align-items: center; align-items: center;
@ -8,4 +17,163 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 11px;
}
.logout-button {
display: inline-flex;
}
.user-info-section {
display: flex;
flex-direction: column;
gap: 16px;
.user-info-section-header {
display: flex;
flex-direction: column;
gap: 4px;
.user-info-section-title {
color: #fff;
font-family: Inter;
font-size: 16px;
font-style: normal;
line-height: 24px; /* 155.556% */
letter-spacing: -0.08px;
}
.user-info-section-subtitle {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
.user-preference-section {
display: flex;
flex-direction: column;
gap: 16px;
.user-preference-section-header {
display: flex;
flex-direction: column;
gap: 4px;
.user-preference-section-title {
color: #fff;
font-family: Inter;
font-size: 16px;
font-style: normal;
line-height: 24px; /* 155.556% */
letter-spacing: -0.08px;
}
.user-preference-section-subtitle {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.user-preference-section-content {
display: flex;
flex-direction: column;
gap: 16px;
.user-preference-section-content-item {
padding: 16px;
border-radius: 4px 4px 0px 0px;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
.user-preference-section-content-item-title-action {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: var(--Vanilla-300, #eee);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: normal;
letter-spacing: -0.07px;
margin-bottom: 8px;
}
.user-preference-section-content-item-description {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}
.reset-password-card {
border-radius: 0px 0px 4px 4px;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
}
.lightMode {
.user-info-section {
.user-info-section-header {
.user-info-section-title {
color: var(--bg-ink-400);
}
.user-info-section-subtitle {
color: var(--bg-ink-300);
}
}
}
.user-preference-section {
.user-preference-section-header {
.user-preference-section-title {
color: var(--bg-ink-400);
}
.user-preference-section-subtitle {
color: var(--bg-ink-300);
}
}
.user-preference-section-content {
.user-preference-section-content-item {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.user-preference-section-content-item-title-action {
color: var(--bg-ink-400);
}
.user-preference-section-content-item-description {
color: var(--bg-ink-300);
}
}
}
}
.reset-password-card {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
} }

View File

@ -73,7 +73,7 @@ function PasswordContainer(): JSX.Element {
currentPassword === updatePassword; currentPassword === updatePassword;
return ( return (
<Card> <Card className="reset-password-card">
<Space direction="vertical" size="small"> <Space direction="vertical" size="small">
<Typography.Title <Typography.Title
level={4} level={4}

View File

@ -1,8 +1,11 @@
.timezone-adaption { .timezone-adaption {
padding: 16px; padding: 16px;
background: var(--bg-ink-400);
border: 1px solid var(--bg-ink-500); border-radius: 4px 4px 0px 0px;
border-radius: 4px; border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
&__header { &__header {
display: flex; display: flex;
@ -20,7 +23,7 @@
&__description { &__description {
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
font-size: 14px; font-size: 12px;
line-height: 20px; line-height: 20px;
margin: 0 0 12px 0; margin: 0 0 12px 0;
} }
@ -52,7 +55,7 @@
align-items: center; align-items: center;
gap: 4px; gap: 4px;
color: var(--bg-robin-400); color: var(--bg-robin-400);
font-size: 14px; font-size: 12px;
line-height: 20px; line-height: 20px;
} }
&__note-text-overridden { &__note-text-overridden {

View File

@ -28,14 +28,16 @@ function TimezoneAdaptation(): JSX.Element {
const handleOverrideClear = (): void => { const handleOverrideClear = (): void => {
updateTimezone(browserTimezone); updateTimezone(browserTimezone);
logEvent('Settings: Timezone override cleared', {}); logEvent('Account Settings: Timezone override cleared', {});
}; };
const handleSwitchChange = (): void => { const handleSwitchChange = (): void => {
setIsAdaptationEnabled((prev) => { setIsAdaptationEnabled((prev) => {
const isEnabled = !prev; const isEnabled = !prev;
logEvent( logEvent(
`Settings: Timezone adaptation ${isEnabled ? 'enabled' : 'disabled'}`, `Account Settings: Timezone adaptation ${
isEnabled ? 'enabled' : 'disabled'
}`,
{}, {},
); );
return isEnabled; return isEnabled;

View File

@ -5,3 +5,231 @@
.userInfo-value { .userInfo-value {
min-width: 20rem; min-width: 20rem;
} }
.user-info-container {
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
padding: 16px;
.user-info-card {
display: flex;
flex-direction: row;
gap: 16px;
}
.user-info-header {
font-size: 13px;
font-weight: 600;
}
.user-info {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
.user-name {
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.user-info-subsection {
display: flex;
flex-direction: row;
gap: 20px;
.user-email {
display: flex;
align-items: center;
gap: 8px;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.user-role {
display: flex;
align-items: center;
gap: 8px;
text-transform: capitalize;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
.user-info-update-section {
display: flex;
flex-direction: row;
gap: 8px;
justify-content: flex-end;
flex-wrap: wrap;
flex: 1;
}
}
.update-name-modal,
.reset-password-modal {
width: 384px !important;
.ant-modal-content {
padding: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
padding: 16px;
background: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-slate-500);
}
.ant-modal-body {
padding: 12px 16px 0px 16px;
.ant-typography {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
}
.update-name-input {
margin-top: 8px;
display: flex;
gap: 8px;
}
.reset-password-container {
display: flex;
flex-direction: column;
gap: 8px;
padding-bottom: 16px;
}
.ant-color-picker-trigger {
padding: 6px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
width: 32px;
height: 32px;
.ant-color-picker-color-block {
border-radius: 50px;
width: 16px;
height: 16px;
flex-shrink: 0;
.ant-color-picker-color-block-inner {
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
.ant-modal-footer {
display: flex;
justify-content: flex-end;
padding: 16px 16px;
margin: 0;
> button {
display: flex;
align-items: center;
border-radius: 2px;
background-color: var(--bg-robin-500) !important;
color: var(--bg-vanilla-100) !important;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 24px;
}
}
}
.title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
.lightMode {
.user-info-container {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.user-info {
.user-name {
color: var(--bg-ink-400);
}
.user-info-subsection {
.user-email {
color: var(--bg-ink-400);
}
.user-role {
color: var(--bg-ink-300);
}
}
}
}
.update-name-modal,
.reset-password-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-modal-header {
background: var(--bg-vanilla-100);
border-bottom: 1px solid var(--bg-vanilla-300);
}
.ant-modal-body {
.ant-typography {
color: var(--bg-ink-400);
}
.ant-color-picker-trigger {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
.title {
color: var(--bg-ink-400);
}
}
}

View File

@ -1,35 +1,115 @@
import '../MySettings.styles.scss'; import '../MySettings.styles.scss';
import './UserInfo.styles.scss'; import './UserInfo.styles.scss';
import { Button, Card, Flex, Input, Space, Typography } from 'antd'; import { Button, Input, Modal, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import changeMyPassword from 'api/v1/factor_password/changeMyPassword';
import editUser from 'api/v1/user/id/update'; import editUser from 'api/v1/user/id/update';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { PencilIcon } from 'lucide-react'; import { Check, FileTerminal, MailIcon, UserIcon } from 'lucide-react';
import { isPasswordValid } from 'pages/SignUp/utils';
import { useAppContext } from 'providers/App/App'; import { useAppContext } from 'providers/App/App';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import APIError from 'types/api/error'; import APIError from 'types/api/error';
import { NameInput } from '../styles';
function UserInfo(): JSX.Element { function UserInfo(): JSX.Element {
const { user, org, updateUser } = useAppContext(); const { user, org, updateUser } = useAppContext();
const { t } = useTranslation(); const { t } = useTranslation(['routes', 'settings', 'common']);
const { notifications } = useNotifications();
const [currentPassword, setCurrentPassword] = useState<string>('');
const [updatePassword, setUpdatePassword] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isPasswordPolicyError, setIsPasswordPolicyError] = useState<boolean>(
false,
);
const [changedName, setChangedName] = useState<string>( const [changedName, setChangedName] = useState<string>(
user?.displayName || '', user?.displayName || '',
); );
const [loading, setLoading] = useState<boolean>(false);
const { notifications } = useNotifications(); const [isUpdateNameModalOpen, setIsUpdateNameModalOpen] = useState<boolean>(
false,
);
const [
isResetPasswordModalOpen,
setIsResetPasswordModalOpen,
] = useState<boolean>(false);
if (!user || !org) { const defaultPlaceHolder = '*************';
useEffect(() => {
if (currentPassword && !isPasswordValid(currentPassword)) {
setIsPasswordPolicyError(true);
} else {
setIsPasswordPolicyError(false);
}
}, [currentPassword]);
if (!user) {
return <div />; return <div />;
} }
const onClickUpdateHandler = async (): Promise<void> => { const hideUpdateNameModal = (): void => {
setIsUpdateNameModalOpen(false);
};
const hideResetPasswordModal = (): void => {
setIsResetPasswordModalOpen(false);
};
const onChangePasswordClickHandler = async (): Promise<void> => {
try { try {
setLoading(true); setIsLoading(true);
if (!isPasswordValid(currentPassword)) {
setIsPasswordPolicyError(true);
setIsLoading(false);
return;
}
await changeMyPassword({
newPassword: updatePassword,
oldPassword: currentPassword,
userId: user.id,
});
notifications.success({
message: t('success', {
ns: 'common',
}),
});
hideResetPasswordModal();
setIsLoading(false);
} catch (error) {
setIsLoading(false);
notifications.error({
message: (error as APIError).error.error.code,
description: (error as APIError).error.error.message,
});
}
};
const isResetPasswordDisabled =
isLoading ||
currentPassword.length === 0 ||
updatePassword.length === 0 ||
isPasswordPolicyError ||
currentPassword === updatePassword;
const onSaveHandler = async (): Promise<void> => {
logEvent('Account Settings: Name Updated', {
name: changedName,
});
logEvent(
'Account Settings: Name Updated',
{
name: changedName,
},
'identify',
);
try {
setIsLoading(true);
await editUser({ await editUser({
displayName: changedName, displayName: changedName,
userId: user.id, userId: user.id,
@ -44,80 +124,143 @@ function UserInfo(): JSX.Element {
...user, ...user,
displayName: changedName, displayName: changedName,
}); });
setLoading(false); setIsLoading(false);
hideUpdateNameModal();
} catch (error) { } catch (error) {
notifications.error({ notifications.error({
message: (error as APIError).getErrorCode(), message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(), description: (error as APIError).getErrorMessage(),
}); });
} }
setLoading(false); setIsLoading(false);
}; };
return ( if (!user || !org) {
<Card> return <div />;
<Space direction="vertical" size="middle"> }
<Flex gap={8}>
<Typography.Title level={4} style={{ marginTop: 0 }}>
User Details
</Typography.Title>
</Flex>
<Flex gap={16}> return (
<Space> <div className="user-info-card">
<Typography className="userInfo-label" data-testid="name-label"> <div className="user-info">
Name <div className="user-name">{user.displayName}</div>
</Typography>
<NameInput <div className="user-info-subsection">
data-testid="name-textbox" <div className="user-email">
placeholder="Your Name" <MailIcon size={16} /> {user.email}
onChange={(event): void => { </div>
setChangedName(event.target.value);
}} <div className="user-role">
value={changedName} <UserIcon size={16} /> {user.role.toLowerCase()}
disabled={loading} </div>
/> </div>
</Space> </div>
<div className="user-info-update-section">
<Button
type="default"
className="periscope-btn secondary"
icon={<FileTerminal size={16} />}
onClick={(): void => setIsUpdateNameModalOpen(true)}
>
Update name
</Button>
<Button <Button
className="flexBtn" type="default"
loading={loading} className="periscope-btn secondary"
disabled={loading} icon={<FileTerminal size={16} />}
onClick={onClickUpdateHandler} onClick={(): void => setIsResetPasswordModalOpen(true)}
data-testid="update-name-button"
type="primary"
> >
<PencilIcon size={12} /> Update Reset password
</Button> </Button>
</Flex> </div>
<Space> <Modal
<Typography className="userInfo-label" data-testid="email-label"> className="update-name-modal"
{' '} title={<span className="title">Update name</span>}
Email{' '} open={isUpdateNameModalOpen}
</Typography> closable
onCancel={hideUpdateNameModal}
footer={[
<Button
key="submit"
type="primary"
icon={<Check size={16} />}
onClick={onSaveHandler}
disabled={isLoading}
data-testid="update-name-btn"
>
Update name
</Button>,
]}
>
<Typography.Text>Name</Typography.Text>
<div className="update-name-input">
<Input <Input
className="userInfo-value" placeholder="e.g. John Doe"
data-testid="email-textbox" value={changedName}
value={user.email} onChange={(e): void => setChangedName(e.target.value)}
disabled
/> />
</Space> </div>
</Modal>
<Space> <Modal
<Typography className="userInfo-label" data-testid="role-label"> className="reset-password-modal"
{' '} title={<span className="title">Reset password</span>}
Role{' '} open={isResetPasswordModalOpen}
</Typography> closable
<Input onCancel={hideResetPasswordModal}
className="userInfo-value" footer={[
value={user.role || ''} <Button
disabled key="submit"
data-testid="role-textbox" className={`periscope-btn ${
isResetPasswordDisabled ? 'secondary' : 'primary'
}`}
icon={<Check size={16} />}
onClick={onChangePasswordClickHandler}
disabled={isLoading || isResetPasswordDisabled}
data-testid="reset-password-btn"
>
Reset password
</Button>,
]}
>
<div className="reset-password-container">
<div className="current-password-input">
<Typography.Text>Current password</Typography.Text>
<Input.Password
data-testid="current-password-textbox"
disabled={isLoading}
placeholder={defaultPlaceHolder}
onChange={(event): void => {
setCurrentPassword(event.target.value);
}}
value={currentPassword}
type="password"
autoComplete="off"
visibilityToggle
/> />
</Space> </div>
</Space>
</Card> <div className="new-password-input">
<Typography.Text>New password</Typography.Text>
<Input.Password
data-testid="new-password-textbox"
disabled={isLoading}
placeholder={defaultPlaceHolder}
onChange={(event): void => {
const updatedValue = event.target.value;
setUpdatePassword(updatedValue);
}}
value={updatePassword}
type="password"
autoComplete="off"
visibilityToggle={false}
/>
</div>
</div>
</Modal>
</div>
); );
} }

View File

@ -2,17 +2,21 @@ import MySettingsContainer from 'container/MySettings';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils'; import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
const toggleThemeFunction = jest.fn(); const toggleThemeFunction = jest.fn();
const logEventFunction = jest.fn();
jest.mock('hooks/useDarkMode', () => ({ jest.mock('hooks/useDarkMode', () => ({
__esModule: true, __esModule: true,
useIsDarkMode: jest.fn(() => ({ useIsDarkMode: jest.fn(() => true),
toggleTheme: toggleThemeFunction,
})),
default: jest.fn(() => ({ default: jest.fn(() => ({
toggleTheme: toggleThemeFunction, toggleTheme: toggleThemeFunction,
})), })),
})); }));
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn((eventName, data) => logEventFunction(eventName, data)),
}));
const errorNotification = jest.fn(); const errorNotification = jest.fn();
const successNotification = jest.fn(); const successNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({ jest.mock('hooks/useNotifications', () => ({
@ -25,90 +29,97 @@ jest.mock('hooks/useNotifications', () => ({
})), })),
})); }));
enum ThemeOptions { const THEME_SELECTOR_TEST_ID = 'theme-selector';
Dark = 'Dark', const RESET_PASSWORD_BUTTON_TEXT = 'Reset password';
Light = 'Light Beta', const CURRENT_PASSWORD_TEST_ID = 'current-password-textbox';
} const NEW_PASSWORD_TEST_ID = 'new-password-textbox';
const UPDATE_NAME_BUTTON_TEST_ID = 'update-name-btn';
const RESET_PASSWORD_BUTTON_TEST_ID = 'reset-password-btn';
const UPDATE_NAME_BUTTON_TEXT = 'Update name';
const PASSWORD_VALIDATION_MESSAGE_TEST_ID = 'password-validation-message';
describe('MySettings Flows', () => { describe('MySettings Flows', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
render(<MySettingsContainer />); render(<MySettingsContainer />);
}); });
describe('Dark/Light Theme Switch', () => { describe('Dark/Light Theme Switch', () => {
it('Should display Dark and Light theme buttons properly', async () => { it('Should display Dark and Light theme options properly', async () => {
// Check Dark theme option
expect(screen.getByText('Dark')).toBeInTheDocument(); expect(screen.getByText('Dark')).toBeInTheDocument();
const darkThemeIcon = screen.getByTestId('dark-theme-icon'); const darkThemeIcon = screen.getByTestId('dark-theme-icon');
expect(darkThemeIcon).toBeInTheDocument(); expect(darkThemeIcon).toBeInTheDocument();
expect(darkThemeIcon.tagName).toBe('svg'); expect(darkThemeIcon.tagName).toBe('svg');
// Check Light theme option
expect(screen.getByText('Light')).toBeInTheDocument(); expect(screen.getByText('Light')).toBeInTheDocument();
const lightThemeIcon = screen.getByTestId('light-theme-icon'); const lightThemeIcon = screen.getByTestId('light-theme-icon');
expect(lightThemeIcon).toBeInTheDocument(); expect(lightThemeIcon).toBeInTheDocument();
expect(lightThemeIcon.tagName).toBe('svg'); expect(lightThemeIcon.tagName).toBe('svg');
expect(screen.getByText('Beta')).toBeInTheDocument();
}); });
it('Should activate Dark and Light buttons on click', async () => { it('Should have Dark theme selected by default', async () => {
const initialSelectedOption = screen.getByRole('radio', { const themeSelector = screen.getByTestId(THEME_SELECTOR_TEST_ID);
name: ThemeOptions.Dark, const darkOption = themeSelector.querySelector(
}); 'input[value="dark"]',
expect(initialSelectedOption).toBeChecked(); ) as HTMLInputElement;
expect(darkOption).toBeChecked();
const newThemeOption = screen.getByRole('radio', {
name: ThemeOptions.Light,
});
fireEvent.click(newThemeOption);
expect(newThemeOption).toBeChecked();
}); });
it('Should switch the them on clicking Light theme', async () => { it('Should switch theme and log event when Light theme is selected', async () => {
const lightThemeOption = screen.getByRole('radio', { const themeSelector = screen.getByTestId(THEME_SELECTOR_TEST_ID);
name: /light/i, const lightOption = themeSelector.querySelector(
}); 'input[value="light"]',
fireEvent.click(lightThemeOption); ) as HTMLInputElement;
fireEvent.click(lightOption);
await waitFor(() => { await waitFor(() => {
expect(toggleThemeFunction).toBeCalled(); expect(toggleThemeFunction).toHaveBeenCalled();
expect(logEventFunction).toHaveBeenCalledWith(
'Account Settings: Theme Changed',
{
theme: 'light',
},
);
}); });
}); });
}); });
describe('User Details Form', () => { describe('User Details Form', () => {
it('Should properly display the User Details Form', () => { it('Should properly display the User Details Form', () => {
const userDetailsHeader = screen.getByRole('heading', { // Open the Update name modal first
name: /user details/i, const updateNameButton = screen.getByText(UPDATE_NAME_BUTTON_TEXT);
}); fireEvent.click(updateNameButton);
const nameLabel = screen.getByTestId('name-label');
const nameTextbox = screen.getByTestId('name-textbox'); // Find the label with class 'ant-typography' and text 'Name'
const updateNameButton = screen.getByTestId('update-name-button'); const nameLabels = screen.getAllByText('Name');
const emailLabel = screen.getByTestId('email-label'); const nameLabel = nameLabels.find((el) =>
const emailTextbox = screen.getByTestId('email-textbox'); el.className.includes('ant-typography'),
const roleLabel = screen.getByTestId('role-label'); );
const roleTextbox = screen.getByTestId('role-textbox'); const nameTextbox = screen.getByPlaceholderText('e.g. John Doe');
const modalUpdateNameButton = screen.getByTestId(UPDATE_NAME_BUTTON_TEST_ID);
expect(userDetailsHeader).toBeInTheDocument();
expect(nameLabel).toBeInTheDocument(); expect(nameLabel).toBeInTheDocument();
expect(nameTextbox).toBeInTheDocument(); expect(nameTextbox).toBeInTheDocument();
expect(updateNameButton).toBeInTheDocument(); expect(modalUpdateNameButton).toBeInTheDocument();
expect(emailLabel).toBeInTheDocument();
expect(emailTextbox).toBeInTheDocument();
expect(roleLabel).toBeInTheDocument();
expect(roleTextbox).toBeInTheDocument();
}); });
it('Should update the name on clicking Update button', async () => { it('Should update the name on clicking Update button', async () => {
const nameTextbox = screen.getByTestId('name-textbox'); // Open the Update name modal first
const updateNameButton = screen.getByTestId('update-name-button'); const updateNameButton = screen.getByText(UPDATE_NAME_BUTTON_TEXT);
fireEvent.click(updateNameButton);
const nameTextbox = screen.getByPlaceholderText('e.g. John Doe');
const modalUpdateNameButton = screen.getByTestId(UPDATE_NAME_BUTTON_TEST_ID);
act(() => { act(() => {
fireEvent.change(nameTextbox, { target: { value: 'New Name' } }); fireEvent.change(nameTextbox, { target: { value: 'New Name' } });
}); });
fireEvent.click(updateNameButton); fireEvent.click(modalUpdateNameButton);
await waitFor(() => await waitFor(() =>
expect(successNotification).toHaveBeenCalledWith({ expect(successNotification).toHaveBeenCalledWith({
@ -119,92 +130,53 @@ describe('MySettings Flows', () => {
}); });
describe('Reset password', () => { describe('Reset password', () => {
let currentPasswordTextbox: Node | Window; it('Should open password reset modal when clicking Reset password button', async () => {
let newPasswordTextbox: Node | Window; const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
let submitButtonElement: HTMLElement; // The first button is the one in the user info section
fireEvent.click(resetPasswordButtons[0]);
beforeEach(() => { // Check if modal is opened (look for modal title)
currentPasswordTextbox = screen.getByTestId('current-password-textbox'); expect(
newPasswordTextbox = screen.getByTestId('new-password-textbox'); screen.getByText((content, element) =>
submitButtonElement = screen.getByTestId('update-password-button'); Boolean(
}); element &&
'className' in element &&
it('Should properly display the Password Reset Form', () => { typeof element.className === 'string' &&
const passwordResetHeader = screen.getByTestId('change-password-header'); element.className.includes('title') &&
expect(passwordResetHeader).toBeInTheDocument(); content === RESET_PASSWORD_BUTTON_TEXT,
),
const currentPasswordLabel = screen.getByTestId('current-password-label'); ),
expect(currentPasswordLabel).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toBeInTheDocument();
expect(currentPasswordTextbox).toBeInTheDocument(); expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toBeInTheDocument();
const newPasswordLabel = screen.getByTestId('new-password-label');
expect(newPasswordLabel).toBeInTheDocument();
expect(newPasswordTextbox).toBeInTheDocument();
expect(submitButtonElement).toBeInTheDocument();
const savePasswordIcon = screen.getByTestId('update-password-icon');
expect(savePasswordIcon).toBeInTheDocument();
expect(savePasswordIcon.tagName).toBe('svg');
}); });
it('Should display validation error if password is less than 8 characters', async () => { it('Should display validation error if password is less than 8 characters', async () => {
const currentPasswordTextbox = screen.getByTestId( const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
'current-password-textbox', fireEvent.click(resetPasswordButtons[0]);
);
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
act(() => { act(() => {
fireEvent.change(currentPasswordTextbox, { target: { value: '123' } }); fireEvent.change(currentPasswordTextbox, { target: { value: '123' } });
}); });
const validationMessage = await screen.findByTestId('validation-message');
await waitFor(() => { await waitFor(() => {
expect(validationMessage).toHaveTextContent( // Use getByTestId for the validation message (if present in your modal/component)
'Password must a have minimum of 8 characters', if (screen.queryByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID)) {
); expect(
screen.getByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID),
).toBeInTheDocument();
}
}); });
}); });
test("Should display 'inavlid credentials' error if different current and new passwords are provided", async () => { it('Should disable reset button when current and new passwords are the same', async () => {
act(() => { const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.change(currentPasswordTextbox, { fireEvent.click(resetPasswordButtons[0]);
target: { value: '123456879' },
});
fireEvent.change(newPasswordTextbox, { target: { value: '123456789' } }); const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
}); const newPasswordTextbox = screen.getByTestId(NEW_PASSWORD_TEST_ID);
const submitButton = screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID);
fireEvent.click(submitButtonElement);
await waitFor(() => expect(errorNotification).toHaveBeenCalled());
});
it('Should check if the "Change Password" button is disabled in case current / new password is less than 8 characters', () => {
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123' },
});
fireEvent.change(newPasswordTextbox, { target: { value: '123' } });
});
expect(submitButtonElement).toBeDisabled();
});
test("Should check if 'Change Password' button is enabled when password is at least 8 characters ", async () => {
expect(submitButtonElement).toBeDisabled();
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123456789' },
});
fireEvent.change(newPasswordTextbox, { target: { value: '1234567890' } });
});
expect(submitButtonElement).toBeEnabled();
});
test("Should check if 'Change Password' button is disabled when current and new passwords are the same ", async () => {
expect(submitButtonElement).toBeDisabled();
act(() => { act(() => {
fireEvent.change(currentPasswordTextbox, { fireEvent.change(currentPasswordTextbox, {
@ -213,7 +185,25 @@ describe('MySettings Flows', () => {
fireEvent.change(newPasswordTextbox, { target: { value: '123456789' } }); fireEvent.change(newPasswordTextbox, { target: { value: '123456789' } });
}); });
expect(submitButtonElement).toBeDisabled(); expect(submitButton).toBeDisabled();
});
it('Should enable reset button when passwords are valid and different', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
const newPasswordTextbox = screen.getByTestId(NEW_PASSWORD_TEST_ID);
const submitButton = screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID);
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123456789' },
});
fireEvent.change(newPasswordTextbox, { target: { value: '987654321' } });
});
expect(submitButton).not.toBeDisabled();
}); });
}); });
}); });

View File

@ -1,18 +1,52 @@
import './MySettings.styles.scss'; import './MySettings.styles.scss';
import { Button, Radio, RadioChangeEvent, Space, Tag, Typography } from 'antd'; import { Radio, RadioChangeEvent, Switch, Tag } from 'antd';
import { Logout } from 'api/utils'; import logEvent from 'api/common/logEvent';
import updateUserPreference from 'api/v1/user/preferences/name/update';
import { AxiosError } from 'axios';
import { USER_PREFERENCES } from 'constants/userPreferences';
import useThemeMode, { useIsDarkMode } from 'hooks/useDarkMode'; import useThemeMode, { useIsDarkMode } from 'hooks/useDarkMode';
import { LogOut, Moon, Sun } from 'lucide-react'; import { useNotifications } from 'hooks/useNotifications';
import { useState } from 'react'; import { Moon, Sun } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { UserPreference } from 'types/api/preferences/preference';
import { showErrorNotification } from 'utils/error';
import Password from './Password';
import TimezoneAdaptation from './TimezoneAdaptation/TimezoneAdaptation'; import TimezoneAdaptation from './TimezoneAdaptation/TimezoneAdaptation';
import UserInfo from './UserInfo'; import UserInfo from './UserInfo';
function MySettings(): JSX.Element { function MySettings(): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const { toggleTheme } = useThemeMode(); const { toggleTheme } = useThemeMode();
const { userPreferences, updateUserPreferenceInContext } = useAppContext();
const { notifications } = useNotifications();
const [sideNavPinned, setSideNavPinned] = useState(false);
useEffect(() => {
if (userPreferences) {
setSideNavPinned(
userPreferences.find(
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
)?.value as boolean,
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userPreferences]);
const {
mutate: updateUserPreferenceMutation,
isLoading: isUpdatingUserPreference,
} = useMutation(updateUserPreference, {
onSuccess: () => {
// No need to do anything on success since we've already updated the state optimistically
},
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
},
});
const themeOptions = [ const themeOptions = [
{ {
@ -39,28 +73,76 @@ function MySettings(): JSX.Element {
const [theme, setTheme] = useState(isDarkMode ? 'dark' : 'light'); const [theme, setTheme] = useState(isDarkMode ? 'dark' : 'light');
const handleThemeChange = ({ target: { value } }: RadioChangeEvent): void => { const handleThemeChange = ({ target: { value } }: RadioChangeEvent): void => {
logEvent('Account Settings: Theme Changed', {
theme: value,
});
setTheme(value); setTheme(value);
toggleTheme(); toggleTheme();
}; };
const handleSideNavPinnedChange = (checked: boolean): void => {
logEvent('Account Settings: Sidebar Pinned Changed', {
pinned: checked,
});
// Optimistically update the UI
setSideNavPinned(checked);
// Update the context immediately
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: checked,
};
updateUserPreferenceInContext(save as UserPreference);
// Make the API call in the background
updateUserPreferenceMutation(
{
name: USER_PREFERENCES.SIDENAV_PINNED,
value: checked,
},
{
onError: (error) => {
// Revert the state if the API call fails
setSideNavPinned(!checked);
updateUserPreferenceInContext({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: !checked,
} as UserPreference);
showErrorNotification(notifications, error as AxiosError);
},
},
);
};
return ( return (
<Space <div className="my-settings-container">
direction="vertical" <div className="user-info-section">
size="large" <div className="user-info-section-header">
style={{ <div className="user-info-section-title">General </div>
margin: '16px 0',
}} <div className="user-info-section-subtitle">
> Manage your account settings.
<div className="theme-selector"> </div>
<Typography.Title </div>
level={5}
style={{ <div className="user-info-container">
margin: '0 0 16px 0', <UserInfo />
}} </div>
> </div>
{' '}
Theme{' '} <div className="user-preference-section">
</Typography.Title> <div className="user-preference-section-header">
<div className="user-preference-section-title">User Preferences</div>
<div className="user-preference-section-subtitle">
Tailor the SigNoz console to work according to your needs.
</div>
</div>
<div className="user-preference-section-content">
<div className="user-preference-section-content-item theme-selector">
<div className="user-preference-section-content-item-title-action">
Select your theme
<Radio.Group <Radio.Group
options={themeOptions} options={themeOptions}
onChange={handleThemeChange} onChange={handleThemeChange}
@ -68,28 +150,35 @@ function MySettings(): JSX.Element {
optionType="button" optionType="button"
buttonStyle="solid" buttonStyle="solid"
data-testid="theme-selector" data-testid="theme-selector"
size="small"
/> />
</div> </div>
<div className="user-info-container"> <div className="user-preference-section-content-item-description">
<UserInfo /> Select if SigNoz&apos;s appearance should be light or dark
</div> </div>
<div className="password-reset-container">
<Password />
</div> </div>
<TimezoneAdaptation /> <TimezoneAdaptation />
<Button <div className="user-preference-section-content-item">
className="flexBtn" <div className="user-preference-section-content-item-title-action">
onClick={(): void => Logout()} Keep the primary sidebar always open{' '}
type="primary" <Switch
data-testid="logout-button" checked={sideNavPinned}
> onChange={handleSideNavPinnedChange}
<LogOut size={12} /> Logout loading={isUpdatingUserPreference}
</Button> />
</Space> </div>
<div className="user-preference-section-content-item-description">
Keep the primary sidebar always open by default, unless collapsed with
the keyboard shortcut
</div>
</div>
</div>
</div>
</div>
); );
} }

View File

@ -0,0 +1,17 @@
.new-explorer-cta {
display: flex;
align-items: center;
color: var(--bg-vanilla-400);
/* Bifrost (Ancient)/Content/sm */
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.ant-btn-icon {
margin-inline-end: 0px;
}
}

View File

@ -5,8 +5,8 @@ export const RIBBON_STYLES = {
}; };
export const buttonText: Record<string, string> = { export const buttonText: Record<string, string> = {
[ROUTES.LOGS_EXPLORER]: 'Switch to Old Logs Explorer', [ROUTES.LOGS_EXPLORER]: 'Old Explorer',
[ROUTES.TRACE]: 'Try new Traces Explorer', [ROUTES.TRACE]: 'New Explorer',
[ROUTES.OLD_LOGS_EXPLORER]: 'Switch to New Logs Explorer', [ROUTES.OLD_LOGS_EXPLORER]: 'New Explorer',
[ROUTES.TRACES_EXPLORER]: 'Switch to Old Trace Explorer', [ROUTES.TRACES_EXPLORER]: 'Old Explorer',
}; };

View File

@ -1,7 +1,9 @@
import { CompassOutlined } from '@ant-design/icons'; import './NewExplorerCTA.styles.scss';
import { Badge, Button } from 'antd'; import { Badge, Button } from 'antd';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import history from 'lib/history'; import history from 'lib/history';
import { Undo } from 'lucide-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
@ -34,11 +36,11 @@ function NewExplorerCTA(): JSX.Element | null {
const button = useMemo( const button = useMemo(
() => ( () => (
<Button <Button
icon={<CompassOutlined />} icon={<Undo size={16} />}
onClick={onClickHandler} onClick={onClickHandler}
danger
data-testid="newExplorerCTA" data-testid="newExplorerCTA"
type="primary" type="text"
className="periscope-btn link"
> >
{buttonText[location.pathname]} {buttonText[location.pathname]}
</Button> </Button>

View File

@ -8,6 +8,7 @@ import updateOrgPreferenceAPI from 'api/v1/org/preferences/name/update';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { InviteTeamMembersProps } from 'container/OrganizationSettings/PendingInvitesContainer'; import { InviteTeamMembersProps } from 'container/OrganizationSettings/PendingInvitesContainer';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
@ -196,7 +197,7 @@ function OnboardingQuestionaire(): JSX.Element {
setUpdatingOrgOnboardingStatus(true); setUpdatingOrgOnboardingStatus(true);
updateOrgPreference({ updateOrgPreference({
name: 'org_onboarding', name: ORG_PREFERENCES.ORG_ONBOARDING,
value: true, value: true,
}); });
}; };

View File

@ -298,8 +298,6 @@
} }
.onboarding-v2 { .onboarding-v2 {
margin: 0px -1rem;
.onboarding-header-container { .onboarding-header-container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@ -0,0 +1,21 @@
.organization-settings-container {
display: flex;
flex-direction: column;
gap: 16px;
margin: 16px auto;
padding: 16px;
width: 90%;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
}
.lightMode {
.organization-settings-container {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}

View File

@ -14,19 +14,19 @@ function OrganizationSettings(): JSX.Element {
} }
return ( return (
<> <div className="organization-settings-container">
<Space direction="vertical"> <Space direction="vertical">
{org.map((e, index) => ( {org.map((e, index) => (
<DisplayName key={e.id} id={e.id} index={index} /> <DisplayName key={e.id} id={e.id} index={index} />
))} ))}
</Space> </Space>
<Divider />
<PendingInvitesContainer /> <PendingInvitesContainer />
<Divider />
<Members /> <Members />
<Divider /> <Divider />
<AuthDomains /> <AuthDomains />
</> </div>
); );
} }

View File

@ -96,7 +96,7 @@ function ServiceMetricTable({
`${range[0]}-${range[1]} of ${total} items`, `${range[0]}-${range[1]} of ${total} items`,
}; };
return ( return (
<> <div className="service-metric-table-container">
{RPS > MAX_RPS_LIMIT && ( {RPS > MAX_RPS_LIMIT && (
<Flex justify="left"> <Flex justify="left">
<Typography.Title level={5} type="warning" style={{ marginTop: 0 }}> <Typography.Title level={5} type="warning" style={{ marginTop: 0 }}>
@ -116,7 +116,7 @@ function ServiceMetricTable({
rowKey="serviceName" rowKey="serviceName"
className="service-metrics-table" className="service-metrics-table"
/> />
</> </div>
); );
} }

View File

@ -53,7 +53,7 @@ function ServiceTraceTable({
`${range[0]}-${range[1]} of ${total} items`, `${range[0]}-${range[1]} of ${total} items`,
}; };
return ( return (
<> <div className="service-traces-table-container">
{RPS > MAX_RPS_LIMIT && ( {RPS > MAX_RPS_LIMIT && (
<Flex justify="left"> <Flex justify="left">
<Typography.Title level={5} type="warning" style={{ marginTop: 0 }}> <Typography.Title level={5} type="warning" style={{ marginTop: 0 }}>
@ -73,7 +73,7 @@ function ServiceTraceTable({
rowKey="serviceName" rowKey="serviceName"
className="service-traces-table" className="service-traces-table"
/> />
</> </div>
); );
} }

View File

@ -5,13 +5,13 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
height: 36px; height: 32px;
margin-bottom: 4px; margin-bottom: 4px;
cursor: pointer; cursor: pointer;
&.active { &.active {
.nav-item-active-marker { .nav-item-active-marker {
background: #3f5ecc; background: #4e74f8;
} }
} }
@ -27,24 +27,24 @@
.nav-item-data { .nav-item-data {
color: white; color: white;
background: #121317; background: var(--Slate-500, #161922);
} }
} }
&.active { &.active {
.nav-item-data { .nav-item-data {
color: white; color: white;
background: #121317; background: var(--Slate-500, #161922);
// color: #3f5ecc; // color: #3f5ecc;
} }
} }
.nav-item-active-marker { .nav-item-active-marker {
margin: 8px 0; margin: 4px 0;
width: 8px; width: 8px;
height: 24px; height: 24px;
background: transparent; background: transparent;
border-radius: 3px; border-radius: 2px;
margin-left: -5px; margin-left: -5px;
} }
@ -53,24 +53,25 @@
max-width: calc(100% - 24px); max-width: calc(100% - 24px);
display: flex; display: flex;
margin: 0px 8px; margin: 0px 8px;
padding: 4px 12px; padding: 2px 8px;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
align-self: stretch; align-self: stretch;
color: #c0c1c3; color: #c0c1c3;
border-radius: 3px;
font-family: Inter; font-family: Inter;
font-size: 13px; font-size: 13px;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 300;
line-height: 18px; line-height: 18px;
background: transparent; background: transparent;
transition: 0.2s all linear; transition: 0.2s all linear;
border-radius: 3px;
.nav-item-icon { .nav-item-icon {
height: 16px; height: 16px;
} }
@ -80,6 +81,31 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
color: #c0c1c3;
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 300;
line-height: normal;
letter-spacing: 0.14px;
}
.nav-item-pin-icon {
margin-left: auto;
cursor: pointer;
display: none;
}
&:hover {
.nav-item-label {
color: var(--Vanilla-100, #fff);
}
.nav-item-pin-icon {
display: block;
}
} }
} }
@ -92,7 +118,7 @@
.nav-item { .nav-item {
&.active { &.active {
.nav-item-active-marker { .nav-item-active-marker {
background: #3f5ecc; background: #4e74f8;
} }
} }
@ -115,6 +141,10 @@
.nav-item-data { .nav-item-data {
color: #121317; color: #121317;
.nav-item-label {
color: var(--bg-ink-400);
}
} }
} }
} }

View File

@ -4,6 +4,7 @@ import './NavItem.styles.scss';
import { Tag } from 'antd'; import { Tag } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import { Pin, PinOff } from 'lucide-react';
import { SidebarItem } from '../sideNav.types'; import { SidebarItem } from '../sideNav.types';
@ -12,14 +13,27 @@ export default function NavItem({
isActive, isActive,
onClick, onClick,
isDisabled, isDisabled,
onTogglePin,
isPinned,
showIcon,
}: { }: {
item: SidebarItem; item: SidebarItem;
isActive: boolean; isActive: boolean;
onClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; onClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
isDisabled: boolean; isDisabled: boolean;
onTogglePin?: (item: SidebarItem) => void;
isPinned?: boolean;
showIcon?: boolean;
}): JSX.Element { }): JSX.Element {
const { label, icon, isBeta, isNew } = item; const { label, icon, isBeta, isNew } = item;
const handleTogglePinClick = (
event: React.MouseEvent<SVGSVGElement, MouseEvent>,
): void => {
event.stopPropagation();
onTogglePin?.(item);
};
return ( return (
<div <div
className={cx( className={cx(
@ -34,15 +48,15 @@ export default function NavItem({
onClick(event); onClick(event);
}} }}
> >
<div className="nav-item-active-marker" /> {showIcon && <div className="nav-item-active-marker" />}
<div className={cx('nav-item-data', isBeta ? 'beta-tag' : '')}> <div className={cx('nav-item-data', isBeta ? 'beta-tag' : '')}>
<div className="nav-item-icon">{icon}</div> {showIcon && <div className="nav-item-icon">{icon}</div>}
<div className="nav-item-label">{label}</div> <div className="nav-item-label">{label}</div>
{isBeta && ( {isBeta && (
<div className="nav-item-beta"> <div className="nav-item-beta">
<Tag bordered={false} color="geekblue"> <Tag bordered={false} className="sidenav-beta-tag">
Beta Beta
</Tag> </Tag>
</div> </div>
@ -55,7 +69,31 @@ export default function NavItem({
</Tag> </Tag>
</div> </div>
)} )}
{onTogglePin && !isPinned && (
<Pin
size={12}
className="nav-item-pin-icon"
onClick={handleTogglePinClick}
color="var(--Vanilla-400, #c0c1c3)"
/>
)}
{onTogglePin && isPinned && (
<PinOff
size={12}
className="nav-item-pin-icon"
onClick={handleTogglePinClick}
color="var(--Vanilla-400, #c0c1c3)"
/>
)}
</div> </div>
</div> </div>
); );
} }
NavItem.defaultProps = {
onTogglePin: undefined,
isPinned: false,
showIcon: false,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,24 +4,30 @@ import {
BarChart2, BarChart2,
BellDot, BellDot,
Binoculars, Binoculars,
Book,
Boxes, Boxes,
BugIcon, BugIcon,
Cloudy, Cloudy,
DraftingCompass, DraftingCompass,
FileKey2, FileKey2,
Github,
Globe,
HardDrive, HardDrive,
Home, Home,
Key,
Keyboard,
Layers2, Layers2,
LayoutGrid, LayoutGrid,
ListMinus, ListMinus,
MessageSquare, MessageSquareText,
Plus,
Receipt, Receipt,
Route, Route,
ScrollText, ScrollText,
Settings, Settings,
Slack, Slack,
Unplug, Unplug,
// Unplug, User,
UserPlus, UserPlus,
} from 'lucide-react'; } from 'lucide-react';
@ -60,11 +66,12 @@ export const manageLicenseMenuItem = {
export const helpSupportMenuItem = { export const helpSupportMenuItem = {
key: ROUTES.SUPPORT, key: ROUTES.SUPPORT,
label: 'Help & Support', label: 'Help & Support',
icon: <MessageSquare size={16} />, icon: <MessageSquareText size={16} />,
}; };
export const shortcutMenuItem = { export const shortcutMenuItem = {
key: ROUTES.SHORTCUTS, key: ROUTES.SHORTCUTS,
// eslint-disable-next-line sonarjs/no-duplicate-string
label: 'Keyboard Shortcuts', label: 'Keyboard Shortcuts',
icon: <Layers2 size={16} />, icon: <Layers2 size={16} />,
}; };
@ -86,79 +93,307 @@ const menuItems: SidebarItem[] = [
key: ROUTES.HOME, key: ROUTES.HOME,
label: 'Home', label: 'Home',
icon: <Home size={16} />, icon: <Home size={16} />,
itemKey: 'home',
}, },
{ {
key: ROUTES.APPLICATION, key: ROUTES.APPLICATION,
label: 'Services', label: 'Services',
icon: <HardDrive size={16} />, icon: <HardDrive size={16} />,
itemKey: 'services',
}, },
{
key: ROUTES.TRACES_EXPLORER,
label: 'Traces',
icon: <DraftingCompass size={16} />,
},
{ {
key: ROUTES.LOGS, key: ROUTES.LOGS,
label: 'Logs', label: 'Logs',
icon: <ScrollText size={16} />, icon: <ScrollText size={16} />,
itemKey: 'logs',
}, },
{ {
key: ROUTES.METRICS_EXPLORER, key: ROUTES.METRICS_EXPLORER,
label: 'Metrics', label: 'Metrics',
icon: <BarChart2 size={16} />, icon: <BarChart2 size={16} />,
isNew: true, isNew: true,
itemKey: 'metrics',
}, },
{ {
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS, key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
label: 'Infra Monitoring', label: 'Infra Monitoring',
icon: <Boxes size={16} />, icon: <Boxes size={16} />,
itemKey: 'infrastructure',
}, },
{ {
key: ROUTES.ALL_DASHBOARD, key: ROUTES.ALL_DASHBOARD,
label: 'Dashboards', label: 'Dashboards',
icon: <LayoutGrid size={16} />, icon: <LayoutGrid size={16} />,
itemKey: 'dashboards',
}, },
{ {
key: ROUTES.MESSAGING_QUEUES_OVERVIEW, key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
label: 'Messaging Queues', label: 'Messaging Queues',
icon: <ListMinus size={16} />, icon: <ListMinus size={16} />,
itemKey: 'messaging-queues',
}, },
{ {
key: ROUTES.API_MONITORING, key: ROUTES.API_MONITORING,
label: 'External APIs', label: 'External APIs',
icon: <Binoculars size={16} />, icon: <Binoculars size={16} />,
isNew: true, isNew: true,
itemKey: 'external-apis',
}, },
{ {
key: ROUTES.LIST_ALL_ALERT, key: ROUTES.LIST_ALL_ALERT,
label: 'Alerts', label: 'Alerts',
icon: <BellDot size={16} />, icon: <BellDot size={16} />,
itemKey: 'alerts',
}, },
{ {
key: ROUTES.INTEGRATIONS, key: ROUTES.INTEGRATIONS,
label: 'Integrations', label: 'Integrations',
icon: <Unplug size={16} />, icon: <Unplug size={16} />,
itemKey: 'integrations',
}, },
{ {
key: ROUTES.ALL_ERROR, key: ROUTES.ALL_ERROR,
label: 'Exceptions', label: 'Exceptions',
icon: <BugIcon size={16} />, icon: <BugIcon size={16} />,
itemKey: 'exceptions',
}, },
{ {
key: ROUTES.SERVICE_MAP, key: ROUTES.SERVICE_MAP,
label: 'Service Map', label: 'Service Map',
icon: <Route size={16} />, icon: <Route size={16} />,
isBeta: true, isBeta: true,
itemKey: 'service-map',
}, },
{ {
key: ROUTES.BILLING, key: ROUTES.BILLING,
label: 'Billing', label: 'Billing',
icon: <Receipt size={16} />, icon: <Receipt size={16} />,
itemKey: 'billing',
}, },
{ {
key: ROUTES.SETTINGS, key: ROUTES.SETTINGS,
label: 'Settings', label: 'Settings',
icon: <Settings size={16} />, icon: <Settings size={16} />,
itemKey: 'settings',
},
];
export const primaryMenuItems: SidebarItem[] = [
{
key: ROUTES.HOME,
label: 'Home',
icon: <Home size={16} />,
itemKey: 'home',
},
{
key: ROUTES.LIST_ALL_ALERT,
label: 'Alerts',
icon: <BellDot size={16} />,
itemKey: 'alerts',
},
{
key: ROUTES.ALL_DASHBOARD,
label: 'Dashboards',
icon: <LayoutGrid size={16} />,
itemKey: 'dashboards',
},
];
export const defaultMoreMenuItems: SidebarItem[] = [
{
key: ROUTES.APPLICATION,
label: 'Services',
icon: <HardDrive size={16} />,
isPinned: true,
isEnabled: true,
itemKey: 'services',
},
{
key: ROUTES.LOGS,
label: 'Logs',
icon: <ScrollText size={16} />,
isPinned: true,
isEnabled: true,
itemKey: 'logs',
},
{
key: ROUTES.TRACES_EXPLORER,
label: 'Traces',
icon: <DraftingCompass size={16} />,
isPinned: true,
isEnabled: true,
itemKey: 'traces',
},
{
key: ROUTES.METRICS_EXPLORER,
label: 'Metrics',
icon: <BarChart2 size={16} />,
isNew: true,
isEnabled: true,
itemKey: 'metrics',
},
{
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
label: 'Infrastructure',
icon: <Boxes size={16} />,
isPinned: true,
isEnabled: true,
itemKey: 'infrastructure',
},
{
key: ROUTES.INTEGRATIONS,
label: 'Integrations',
icon: <Unplug size={16} />,
isEnabled: true,
itemKey: 'integrations',
},
{
key: ROUTES.ALL_ERROR,
label: 'Exceptions',
icon: <BugIcon size={16} />,
isEnabled: true,
itemKey: 'exceptions',
},
{
key: ROUTES.API_MONITORING,
label: 'External APIs',
icon: <Binoculars size={16} />,
isNew: true,
isEnabled: true,
itemKey: 'external-apis',
},
{
key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
label: 'Messaging Queues',
icon: <ListMinus size={16} />,
isEnabled: true,
itemKey: 'messaging-queues',
},
{
key: ROUTES.SERVICE_MAP,
label: 'Service Map',
icon: <Route size={16} />,
isEnabled: true,
itemKey: 'service-map',
},
];
export const settingsMenuItems: SidebarItem[] = [
{
key: ROUTES.SETTINGS,
label: 'General',
icon: <Settings size={16} />,
isEnabled: true,
itemKey: 'general',
},
{
key: ROUTES.BILLING,
label: 'Billing',
icon: <Receipt size={16} />,
isEnabled: false,
itemKey: 'billing',
},
{
key: ROUTES.ORG_SETTINGS,
label: 'Members & SSO',
icon: <User size={16} />,
isEnabled: false,
itemKey: 'members-sso',
},
{
key: ROUTES.CUSTOM_DOMAIN_SETTINGS,
label: 'Custom Domain',
icon: <Globe size={16} />,
isEnabled: false,
itemKey: 'custom-domain',
},
{
key: ROUTES.INTEGRATIONS,
label: 'Integrations',
icon: <Unplug size={16} />,
isEnabled: false,
itemKey: 'integrations',
},
{
key: ROUTES.ALL_CHANNELS,
label: 'Notification Channels',
icon: <FileKey2 size={16} />,
isEnabled: true,
itemKey: 'notification-channels',
},
{
key: ROUTES.API_KEYS,
label: 'API Keys',
icon: <Key size={16} />,
isEnabled: false,
itemKey: 'api-keys',
},
{
key: ROUTES.INGESTION_SETTINGS,
label: 'Ingestion',
icon: <RocketOutlined rotate={45} />,
isEnabled: false,
itemKey: 'ingestion',
},
{
key: ROUTES.MY_SETTINGS,
label: 'Account Settings',
icon: <User size={16} />,
isEnabled: true,
itemKey: 'account-settings',
},
{
key: ROUTES.SHORTCUTS,
label: 'Keyboard Shortcuts',
icon: <Layers2 size={16} />,
isEnabled: true,
itemKey: 'keyboard-shortcuts',
},
];
export const helpSupportDropdownMenuItems: SidebarItem[] = [
{
key: 'documentation',
label: 'Documentation',
icon: <Book size={14} />,
isExternal: true,
url: 'https://signoz.io/docs',
itemKey: 'documentation',
},
{
key: 'github',
label: 'GitHub',
icon: <Github size={14} />,
isExternal: true,
url: 'https://github.com/signoz/signoz',
itemKey: 'github',
},
{
key: 'slack',
label: 'Community Slack',
icon: <Slack size={14} />,
isExternal: true,
url: 'https://signoz.io/slack',
itemKey: 'community-slack',
},
{
key: 'chat-support',
label: 'Chat with Support',
icon: <MessageSquareText size={14} />,
itemKey: 'chat-support',
},
{
key: ROUTES.SHORTCUTS,
label: 'Keyboard Shortcuts',
icon: <Keyboard size={14} />,
itemKey: 'keyboard-shortcuts',
},
{
key: 'invite-collaborators',
label: 'Invite a Collaborator',
icon: <Plus size={14} />,
itemKey: 'invite-collaborators',
}, },
]; ];

View File

@ -8,12 +8,18 @@ export type SidebarMenu = MenuItem & {
}; };
export interface SidebarItem { export interface SidebarItem {
key: string | number;
icon?: ReactNode; icon?: ReactNode;
text?: ReactNode; text?: ReactNode;
key: string | number;
label?: ReactNode; label?: ReactNode;
isBeta?: boolean; isBeta?: boolean;
isNew?: boolean; isNew?: boolean;
isPinned?: boolean;
children?: SidebarItem[];
isExternal?: boolean;
url?: string;
isEnabled?: boolean;
itemKey?: string;
} }
export enum SecondaryMenuItemKey { export enum SecondaryMenuItemKey {

View File

@ -231,6 +231,9 @@ export const routesToSkip = [
ROUTES.CHANNELS_EDIT, ROUTES.CHANNELS_EDIT,
ROUTES.WORKSPACE_ACCESS_RESTRICTED, ROUTES.WORKSPACE_ACCESS_RESTRICTED,
ROUTES.ALL_ERROR, ROUTES.ALL_ERROR,
ROUTES.UN_AUTHORIZED,
ROUTES.NOT_FOUND,
ROUTES.SOMETHING_WENT_WRONG,
]; ];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS]; export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@ -0,0 +1,4 @@
.top-nav-container {
padding: 0px 8px;
margin-bottom: 16px;
}

View File

@ -1,3 +1,5 @@
import './TopNav.styles.scss';
import { Col, Row, Space } from 'antd'; import { Col, Row, Space } from 'antd';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useMemo } from 'react'; import { useMemo } from 'react';
@ -43,7 +45,7 @@ function TopNav(): JSX.Element | null {
} }
return !isRouteToSkip ? ( return !isRouteToSkip ? (
<Row style={{ marginBottom: '1rem' }}> <div className="top-nav-container">
<Col span={24} style={{ marginTop: '1rem' }}> <Col span={24} style={{ marginTop: '1rem' }}>
<Row justify="end"> <Row justify="end">
<Space align="center" size={16} direction="horizontal"> <Space align="center" size={16} direction="horizontal">
@ -54,7 +56,7 @@ function TopNav(): JSX.Element | null {
</Space> </Space>
</Row> </Row>
</Col> </Col>
</Row> </div>
) : null; ) : null;
} }

View File

@ -0,0 +1,122 @@
.version-container {
max-height: 100vh;
overflow: hidden;
.version-page-header {
border-bottom: 1px solid var(--Slate-500, #161922);
background: rgba(11, 12, 14, 0.7);
backdrop-filter: blur(20px);
.version-page-header-title {
color: var(--Vanilla-100, #fff);
text-align: center;
font-family: Inter;
font-size: 13px;
font-style: normal;
line-height: 14px;
letter-spacing: 0.4px;
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
}
}
.version-page-container {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
.version-card {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
border-radius: 4px 4px 0px 0px;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
}
.version-page-form {
display: flex;
}
.version-page-stale-version-container {
padding: 16px;
background-color: rgba(78, 116, 248, 0.1);
border-radius: 4px;
.version-page-stale-version-container-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-vanilla-100);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 14px; /* 150% */
letter-spacing: 0.4px;
}
}
.version-page-latest-version-container {
.version-page-latest-version-container-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 14px; /* 150% */
letter-spacing: 0.4px;
}
}
.version-page-upgrade-container {
display: flex;
justify-content: flex-start;
margin-top: 16px;
}
}
}
.lightMode {
.version-container {
.version-page-header {
border-bottom: 1px solid var(--bg-vanilla-300);
background: #fff;
.version-page-header-title {
color: var(--bg-ink-400);
}
}
.version-page-container {
.version-card {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
.version-page-stale-version-container {
.version-page-stale-version-container-title {
color: var(--text-ink-400);
}
}
.version-page-latest-version-container {
.version-page-latest-version-container-title {
color: var(--text-ink-400);
}
}
}
}
}

View File

@ -1,5 +1,7 @@
import { WarningFilled } from '@ant-design/icons'; import './Version.styles.scss';
import { Button, Card, Form, Space, Typography } from 'antd';
import { Button, Form } from 'antd';
import { CheckCircle, CloudUpload, InfoIcon, Wrench } from 'lucide-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@ -34,11 +36,16 @@ function Version(): JSX.Element {
); );
return ( return (
<Card style={{ margin: '16px 0' }}> <div className="version-container">
<Typography.Title ellipsis level={4} style={{ marginTop: 0 }}> <header className="version-page-header">
{t('version')} <div className="version-page-header-title">
</Typography.Title> <Wrench size={16} />
Version
</div>
</header>
<div className="version-page-container">
<div className="version-card">
<Form <Form
wrapperCol={{ wrapperCol={{
span: 14, span: 14,
@ -71,36 +78,40 @@ function Version(): JSX.Element {
</Form> </Form>
{!isError && isLatestVersion && ( {!isError && isLatestVersion && (
<div> <div className="version-page-latest-version-container">
<Space align="start"> <div className="version-page-latest-version-container-title">
<span></span> <CheckCircle size={16} />
<Typography.Paragraph italic>
{t('latest_version_signoz')} {t('latest_version_signoz')}
</Typography.Paragraph> </div>
</Space>
</div> </div>
)} )}
{!isError && !isLatestVersion && ( {!isError && !isLatestVersion && (
<div> <div className="version-page-stale-version-container">
<Space align="start"> <div className="version-page-stale-version-container-title">
<span> <InfoIcon size={16} />
<WarningFilled style={{ color: '#E87040' }} /> {t('stale_version')}
</span> </div>
<Typography.Paragraph italic>{t('stale_version')}</Typography.Paragraph>
</Space>
</div> </div>
)} )}
{!isError && !isLatestVersion && ( {!isError && !isLatestVersion && (
<div className="version-page-upgrade-container">
<Button <Button
href="https://signoz.io/docs/operate/docker-standalone/#upgrade" href="https://signoz.io/docs/operate/docker-standalone/#upgrade"
target="_blank" target="_blank"
type="primary"
className="periscope-btn primary"
icon={<CloudUpload size={16} />}
> >
{t('read_how_to_upgrade')} {t('read_how_to_upgrade')}
</Button> </Button>
</div>
)} )}
</Card> </div>
</div>
</div>
); );
} }

View File

@ -9,6 +9,11 @@
<meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" /> <meta http-equiv="Expires" content="0" />
<!-- Preconnect to CDNs -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://cdn.vercel.com" crossorigin />
<title data-react-helmet="true"> <title data-react-helmet="true">
Open source Observability platform | SigNoz Open source Observability platform | SigNoz
</title> </title>
@ -53,6 +58,17 @@
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" /> <link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/uPlot.min.css" /> <link rel="stylesheet" href="/css/uPlot.min.css" />
<% if (htmlWebpackPlugin.options.templateParameters.preloadFonts) { %> <%
htmlWebpackPlugin.options.templateParameters.preloadFonts.forEach(function(font)
{ %>
<link
rel="preload"
href="<%= font.href %>"
as="<%= font.as %>"
type="<%= font.type %>"
crossorigin="<%= font.crossorigin %>"
/>
<% }); %> <% } %>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -9,7 +9,6 @@ import TimezoneProvider from 'providers/Timezone';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import store from 'store'; import store from 'store';
@ -47,9 +46,6 @@ if (container) {
<AppRoutes /> <AppRoutes />
</AppProvider> </AppProvider>
</Provider> </Provider>
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider> </QueryClientProvider>
</TimezoneProvider> </TimezoneProvider>
</ThemeProvider> </ThemeProvider>

View File

@ -95,7 +95,7 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
); );
} }
return ( return (
<Container> <div className="service-map-container">
<ResourceAttributesFilter <ResourceAttributesFilter
suffixIcon={ suffixIcon={
<TextToolTip <TextToolTip
@ -109,7 +109,7 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
/> />
<Map fgRef={fgRef} serviceMap={serviceMap} /> <Map fgRef={fgRef} serviceMap={serviceMap} />
</Container> </div>
); );
} }

View File

@ -0,0 +1,10 @@
.alerts-container {
.ant-tabs-nav-wrap {
padding-left: 16px;
}
.ant-tabs-content-holder {
padding-left: 16px;
padding-right: 16px;
}
}

View File

@ -1,3 +1,5 @@
import './AlertList.styles.scss';
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import { TabsProps } from 'antd/lib'; import { TabsProps } from 'antd/lib';
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon'; import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
@ -70,7 +72,7 @@ function AllAlertList(): JSX.Element {
} }
safeNavigate(`/alerts?${params}`); safeNavigate(`/alerts?${params}`);
}} }}
className={`${ className={`alerts-container ${
isAlertHistory || isAlertOverview ? 'alert-details-tabs' : '' isAlertHistory || isAlertOverview ? 'alert-details-tabs' : ''
}`} }`}
/> />

View File

@ -0,0 +1,15 @@
.edit-alert-channels-container {
width: 90%;
margin: 12px auto;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
padding: 16px;
.form-alert-channels-title {
margin-top: 0px;
margin-bottom: 16px;
}
}

View File

@ -1,4 +1,7 @@
/* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable sonarjs/cognitive-complexity */
import './ChannelsEdit.styles.scss';
import { Typography } from 'antd'; import { Typography } from 'antd';
import get from 'api/channels/get'; import get from 'api/channels/get';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
@ -128,6 +131,7 @@ function ChannelsEdit(): JSX.Element {
const target = prepChannelConfig(); const target = prepChannelConfig();
return ( return (
<div className="edit-alert-channels-container">
<EditAlertChannels <EditAlertChannels
{...{ {...{
initialValue: { initialValue: {
@ -137,6 +141,7 @@ function ChannelsEdit(): JSX.Element {
}, },
}} }}
/> />
</div>
); );
} }
interface Params { interface Params {

View File

@ -14,8 +14,9 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.request-entity-container { .request-entity-container {
margin: 0; margin: 16px 0px;
border-right: none; border-right: none;
border-left: none; border-left: none;
} }

View File

@ -82,7 +82,7 @@ function OldLogsExplorer(): JSX.Element {
}; };
return ( return (
<> <div className="old-logs-explorer">
<SpaceContainer <SpaceContainer
split={<Divider type="vertical" />} split={<Divider type="vertical" />}
align="center" align="center"
@ -144,7 +144,7 @@ function OldLogsExplorer(): JSX.Element {
</Row> </Row>
<LogDetailedView /> <LogDetailedView />
</> </div>
); );
} }

View File

@ -138,9 +138,9 @@ describe('Logs Explorer Tests', () => {
expect(timeSeriesView).toBeInTheDocument(); expect(timeSeriesView).toBeInTheDocument();
expect(tableView).toBeInTheDocument(); expect(tableView).toBeInTheDocument();
// check the presence of old logs explorer CTA // // check the presence of old logs explorer CTA - TODO: add this once we have the header updated
const oldLogsCTA = getByText('Switch to Old Logs Explorer'); // const oldLogsCTA = getByText('Switch to Old Logs Explorer');
expect(oldLogsCTA).toBeInTheDocument(); // expect(oldLogsCTA).toBeInTheDocument();
}); });
// update this test properly // update this test properly

View File

@ -47,6 +47,7 @@ function ApDexApplication(): JSX.Element {
showArrow={false} showArrow={false}
open={isOpen} open={isOpen}
onOpenChange={handleOpenChange} onOpenChange={handleOpenChange}
overlayClassName="ap-dex-settings-popover"
content={ content={
<ApDexSettings <ApDexSettings
servicename={servicename} servicename={servicename}
@ -57,9 +58,11 @@ function ApDexApplication(): JSX.Element {
/> />
} }
> >
<div className="ap-dex-settings-popover-content">
<Button size="middle" icon={<SettingOutlined />}> <Button size="middle" icon={<SettingOutlined />}>
Settings Settings
</Button> </Button>
</div>
</Popover> </Popover>
); );
} }

View File

@ -59,11 +59,11 @@ function MetricsApplication(): JSX.Element {
); );
return ( return (
<> <div className="metrics-application-container">
<ResourceAttributesFilter /> <ResourceAttributesFilter />
<ApDexApplication /> <ApDexApplication />
<RouteTab routes={routes} history={history} activeKey={activeKey} /> <RouteTab routes={routes} history={history} activeKey={activeKey} />
</> </div>
); );
} }

View File

@ -19,6 +19,7 @@
.ant-tabs-content-holder { .ant-tabs-content-holder {
display: flex; display: flex;
padding: 16px;
.ant-tabs-content { .ant-tabs-content {
flex: 1; flex: 1;

View File

@ -0,0 +1,104 @@
.settings-page {
max-height: 100vh;
overflow: hidden;
.settings-page-header {
border-bottom: 1px solid var(--Slate-500, #161922);
background: rgba(11, 12, 14, 0.7);
backdrop-filter: blur(20px);
.settings-page-header-title {
color: var(--Vanilla-100, #fff);
text-align: center;
font-family: Inter;
font-size: 13px;
font-style: normal;
line-height: 14px;
letter-spacing: 0.4px;
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
}
}
.settings-page-content-container {
display: flex;
flex-direction: row;
align-items: flex-start;
.settings-page-sidenav {
width: 240px;
height: calc(100vh - 48px);
border-right: 1px solid var(--Slate-500, #161922);
background: var(--Ink-500, #0b0c0e);
padding: 10px 8px;
}
.settings-page-content {
flex: 1;
height: calc(100vh - 48px);
background: var(--Ink-500, #0b0c0e);
padding: 10px 8px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
}
}
.lightMode {
.settings-page {
.settings-page-header {
border-bottom: 1px solid var(--bg-vanilla-300);
background: #fff;
backdrop-filter: blur(20px);
.settings-page-header-title {
color: var(--bg-ink-400);
background: var(--bg-vanilla-100);
border-right: 1px solid var(--bg-vanilla-300);
}
}
.settings-page-content-container {
.settings-page-sidenav {
border-right: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
.settings-page-content {
background: var(--bg-vanilla-100);
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
}
}
}

View File

@ -0,0 +1,234 @@
import './Settings.styles.scss';
import logEvent from 'api/common/logEvent';
import RouteTab from 'components/RouteTab';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import { routeConfig } from 'container/SideNav/config';
import { getQueryString } from 'container/SideNav/helper';
import { settingsMenuItems as defaultSettingsMenuItems } from 'container/SideNav/menuItems';
import NavItem from 'container/SideNav/NavItem/NavItem';
import { SidebarItem } from 'container/SideNav/sideNav.types';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { Wrench } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { USER_ROLES } from 'types/roles';
import { getRoutes } from './utils';
function SettingsPage(): JSX.Element {
const { pathname, search } = useLocation();
const { user, featureFlags, trialInfo } = useAppContext();
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
const [settingsMenuItems, setSettingsMenuItems] = useState<SidebarItem[]>(
defaultSettingsMenuItems,
);
const isAdmin = user.role === USER_ROLES.ADMIN;
const isEditor = user.role === USER_ROLES.EDITOR;
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
const [isCurrentOrgSettings] = useComponentPermission(
['current_org_settings'],
user.role,
);
const { t } = useTranslation(['routes']);
const isGatewayEnabled =
featureFlags?.find((feature) => feature.name === FeatureKeys.GATEWAY)
?.active || false;
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
setSettingsMenuItems((prevItems) => {
let updatedItems = [...prevItems];
if (isCloudUser) {
if (isAdmin) {
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.BILLING ||
item.key === ROUTES.INTEGRATIONS ||
item.key === ROUTES.CUSTOM_DOMAIN_SETTINGS ||
item.key === ROUTES.API_KEYS ||
item.key === ROUTES.INGESTION_SETTINGS ||
item.key === ROUTES.ORG_SETTINGS
? true
: item.isEnabled,
}));
}
if (isEditor) {
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.INGESTION_SETTINGS ||
item.key === ROUTES.INTEGRATIONS
? true
: item.isEnabled,
}));
}
}
if (isEnterpriseSelfHostedUser) {
if (isAdmin) {
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.BILLING ||
item.key === ROUTES.INTEGRATIONS ||
item.key === ROUTES.API_KEYS ||
item.key === ROUTES.ORG_SETTINGS
? true
: item.isEnabled,
}));
}
if (isEditor) {
// eslint-disable-next-line sonarjs/no-identical-functions
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled: item.key === ROUTES.INTEGRATIONS ? true : item.isEnabled,
}));
}
}
if (!isCloudUser && !isEnterpriseSelfHostedUser) {
if (isAdmin) {
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.API_KEYS || item.key === ROUTES.ORG_SETTINGS
? true
: item.isEnabled,
}));
}
// disable billing and integrations for non-cloud users
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.BILLING || item.key === ROUTES.INTEGRATIONS
? false
: item.isEnabled,
}));
}
return updatedItems;
});
}, [isAdmin, isEditor, isCloudUser, isEnterpriseSelfHostedUser]);
const routes = useMemo(
() =>
getRoutes(
user.role,
isCurrentOrgSettings,
isGatewayEnabled,
isWorkspaceBlocked,
isCloudUser,
isEnterpriseSelfHostedUser,
t,
),
[
user.role,
isCurrentOrgSettings,
isGatewayEnabled,
isWorkspaceBlocked,
isCloudUser,
isEnterpriseSelfHostedUser,
t,
],
);
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
const openInNewTab = (path: string): void => {
window.open(path, '_blank');
};
const onClickHandler = useCallback(
(key: string, event: MouseEvent | null) => {
const params = new URLSearchParams(search);
const availableParams = routeConfig[key];
const queryString = getQueryString(availableParams || [], params);
if (pathname !== key) {
if (event && isCtrlMetaKey(event)) {
openInNewTab(`${key}?${queryString.join('&')}`);
} else {
history.push(`${key}?${queryString.join('&')}`, {
from: pathname,
});
}
}
},
[pathname, search],
);
const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => {
onClickHandler(item?.key as string, event);
};
const isActiveNavItem = (key: string): boolean => {
if (pathname.startsWith(ROUTES.ALL_CHANNELS) && key === ROUTES.ALL_CHANNELS) {
return true;
}
return pathname === key;
};
return (
<div className="settings-page">
<header className="settings-page-header">
<div className="settings-page-header-title">
<Wrench size={16} />
Settings
</div>
</header>
<div className="settings-page-content-container">
<div className="settings-page-sidenav">
{settingsMenuItems
.filter((item) => item.isEnabled)
.map((item) => (
<NavItem
key={item.key}
item={item}
isActive={isActiveNavItem(item.key as string)}
isDisabled={false}
showIcon={false}
onClick={(event): void => {
logEvent('Settings V2: Menu clicked', {
menuLabel: item.label,
menuRoute: item.key,
});
handleMenuItemClick((event as unknown) as MouseEvent, item);
}}
/>
))}
</div>
<div className="settings-page-content">
<RouteTab
routes={routes}
activeKey={pathname}
history={history}
tabBarStyle={{ display: 'none' }}
/>
</div>
</div>
</div>
);
}
export default SettingsPage;

View File

@ -2,11 +2,15 @@ import { RouteTabProps } from 'components/RouteTab/types';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import AlertChannels from 'container/AllAlertChannels'; import AlertChannels from 'container/AllAlertChannels';
import APIKeys from 'container/APIKeys/APIKeys'; import APIKeys from 'container/APIKeys/APIKeys';
import BillingContainer from 'container/BillingContainer/BillingContainer';
import CreateAlertChannels from 'container/CreateAlertChannels';
import { ChannelType } from 'container/CreateAlertChannels/config';
import CustomDomainSettings from 'container/CustomDomainSettings'; import CustomDomainSettings from 'container/CustomDomainSettings';
import GeneralSettings from 'container/GeneralSettings'; import GeneralSettings from 'container/GeneralSettings';
import GeneralSettingsCloud from 'container/GeneralSettingsCloud'; import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
import IngestionSettings from 'container/IngestionSettings/IngestionSettings'; import IngestionSettings from 'container/IngestionSettings/IngestionSettings';
import MultiIngestionSettings from 'container/IngestionSettings/MultiIngestionSettings'; import MultiIngestionSettings from 'container/IngestionSettings/MultiIngestionSettings';
import MySettings from 'container/MySettings';
import OrganizationSettings from 'container/OrganizationSettings'; import OrganizationSettings from 'container/OrganizationSettings';
import { TFunction } from 'i18next'; import { TFunction } from 'i18next';
import { import {
@ -14,9 +18,16 @@ import {
BellDot, BellDot,
Building, Building,
Cpu, Cpu,
CreditCard,
Globe, Globe,
Keyboard,
KeySquare, KeySquare,
Pencil,
Plus,
User,
} from 'lucide-react'; } from 'lucide-react';
import ChannelsEdit from 'pages/ChannelsEdit';
import Shortcuts from 'pages/Shortcuts';
export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [ export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [
{ {
@ -123,3 +134,70 @@ export const customDomainSettings = (t: TFunction): RouteTabProps['routes'] => [
key: ROUTES.CUSTOM_DOMAIN_SETTINGS, key: ROUTES.CUSTOM_DOMAIN_SETTINGS,
}, },
]; ];
export const billingSettings = (t: TFunction): RouteTabProps['routes'] => [
{
Component: BillingContainer,
name: (
<div className="periscope-tab">
<CreditCard size={16} /> {t('routes:billing').toString()}
</div>
),
route: ROUTES.BILLING,
key: ROUTES.BILLING,
},
];
export const keyboardShortcuts = (t: TFunction): RouteTabProps['routes'] => [
{
Component: Shortcuts,
name: (
<div className="periscope-tab">
<Keyboard size={16} /> {t('routes:shortcuts').toString()}
</div>
),
route: ROUTES.SHORTCUTS,
key: ROUTES.SHORTCUTS,
},
];
export const mySettings = (t: TFunction): RouteTabProps['routes'] => [
{
Component: MySettings,
name: (
<div className="periscope-tab">
<User size={16} /> {t('routes:my_settings').toString()}
</div>
),
route: ROUTES.MY_SETTINGS,
key: ROUTES.MY_SETTINGS,
},
];
export const createAlertChannels = (t: TFunction): RouteTabProps['routes'] => [
{
Component: (): JSX.Element => (
<CreateAlertChannels preType={ChannelType.Slack} />
),
name: (
<div className="periscope-tab">
<Plus size={16} /> {t('routes:create_alert_channels').toString()}
</div>
),
route: ROUTES.CHANNELS_NEW,
key: ROUTES.CHANNELS_NEW,
},
];
export const editAlertChannels = (t: TFunction): RouteTabProps['routes'] => [
{
Component: ChannelsEdit,
name: (
<div className="periscope-tab">
<Pencil size={16} /> {t('routes:edit_alert_channels').toString()}
</div>
),
route: ROUTES.CHANNELS_EDIT,
key: ROUTES.CHANNELS_EDIT,
},
];

View File

@ -1,55 +1,3 @@
import RouteTab from 'components/RouteTab'; import SettingsPage from './Settings';
import { FeatureKeys } from 'constants/features';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { getRoutes } from './utils';
function SettingsPage(): JSX.Element {
const { pathname } = useLocation();
const { user, featureFlags, trialInfo } = useAppContext();
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
const [isCurrentOrgSettings] = useComponentPermission(
['current_org_settings'],
user.role,
);
const { t } = useTranslation(['routes']);
const isGatewayEnabled =
featureFlags?.find((feature) => feature.name === FeatureKeys.GATEWAY)
?.active || false;
const routes = useMemo(
() =>
getRoutes(
user.role,
isCurrentOrgSettings,
isGatewayEnabled,
isWorkspaceBlocked,
isCloudUser,
isEnterpriseSelfHostedUser,
t,
),
[
user.role,
isCurrentOrgSettings,
isGatewayEnabled,
isWorkspaceBlocked,
isCloudUser,
isEnterpriseSelfHostedUser,
t,
],
);
return <RouteTab routes={routes} activeKey={pathname} history={history} />;
}
export default SettingsPage; export default SettingsPage;

View File

@ -5,10 +5,15 @@ import { ROLES, USER_ROLES } from 'types/roles';
import { import {
alertChannels, alertChannels,
apiKeys, apiKeys,
billingSettings,
createAlertChannels,
customDomainSettings, customDomainSettings,
editAlertChannels,
generalSettings, generalSettings,
ingestionSettings, ingestionSettings,
keyboardShortcuts,
multiIngestionSettings, multiIngestionSettings,
mySettings,
organizationSettings, organizationSettings,
} from './config'; } from './config';
@ -52,9 +57,16 @@ export const getRoutes = (
settings.push(...apiKeys(t)); settings.push(...apiKeys(t));
} }
if (isCloudUser && isAdmin) { if ((isCloudUser || isEnterpriseSelfHostedUser) && isAdmin) {
settings.push(...customDomainSettings(t)); settings.push(...customDomainSettings(t), ...billingSettings(t));
} }
settings.push(
...mySettings(t),
...createAlertChannels(t),
...editAlertChannels(t),
...keyboardShortcuts(t),
);
return settings; return settings;
}; };

View File

@ -3,21 +3,19 @@
flex-direction: column; flex-direction: column;
margin-top: 1rem; margin-top: 1rem;
padding: 1rem; padding: 1rem;
gap: 50px; gap: 24px;
width: 80%;
margin: 0 auto;
.shortcut-section { .shortcut-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 12px;
.shortcut-section-heading { .shortcut-section-heading {
font-weight: 600; font-weight: 600;
font-size: 22px; font-size: 22px;
line-height: 1.3636363636363635; line-height: 1.3636363636363635;
} }
.shortcut-section-table {
width: 70%;
}
} }
} }

View File

@ -12,10 +12,11 @@ function SomethingWentWrong(): JSX.Element {
<Button <Button
type="primary" type="primary"
onClick={(): void => { onClick={(): void => {
history.push(ROUTES.APPLICATION); history.push(ROUTES.HOME);
}} }}
className="periscope-btn primary"
> >
Return to Services page Return to Home
</Button> </Button>
</Container> </Container>
); );

View File

@ -183,7 +183,7 @@ export default function Support(): JSX.Element {
return ( return (
<div className="support-page-container"> <div className="support-page-container">
<div className="support-page-header"> <div className="support-page-header">
<Title level={3}> Support </Title> <Title level={3}> Help & Support </Title>
<Text style={{ fontSize: 14 }}> <Text style={{ fontSize: 14 }}>
We are here to help in case of questions or issues. Pick the channel that We are here to help in case of questions or issues. Pick the channel that
is most convenient for you. is most convenient for you.

View File

@ -11,8 +11,9 @@ function UnAuthorizePage(): JSX.Element {
<Typography.Title level={3}> <Typography.Title level={3}>
Oops.. you don&apos;t have permission to view this page Oops.. you don&apos;t have permission to view this page
</Typography.Title> </Typography.Title>
<Button to={ROUTES.APPLICATION} tabIndex={0}>
Return To Services Page <Button to={ROUTES.HOME} tabIndex={0} className="periscope-btn primary">
Return To Home
</Button> </Button>
</Space> </Space>
</Container> </Container>

View File

@ -38,12 +38,20 @@
} }
&.link { &.link {
color: var(--bg-vanilla-400); color: var(--Vanilla-400, #c0c1c3);
border: none; border: none;
box-shadow: none; box-shadow: none;
background: transparent; background: transparent;
font-size: 11px; font-size: 12px;
font-weight: 400; font-weight: 400;
letter-spacing: -0.07px;
line-height: 20px;
&:hover {
color: var(--bg-vanilla-100) !important;
background-color: transparent !important;
}
} }
&.success { &.success {

View File

@ -1,6 +1,7 @@
import getLocalStorageApi from 'api/browser/localstorage/get'; import getLocalStorageApi from 'api/browser/localstorage/get';
import { Logout } from 'api/utils'; import { Logout } from 'api/utils';
import listOrgPreferences from 'api/v1/org/preferences/list'; import listOrgPreferences from 'api/v1/org/preferences/list';
import listUserPreferences from 'api/v1/user/preferences/list';
import getUserVersion from 'api/v1/version/getVersion'; import getUserVersion from 'api/v1/version/getVersion';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -25,7 +26,10 @@ import {
LicenseState, LicenseState,
TrialInfo, TrialInfo,
} from 'types/api/licensesV3/getActive'; } from 'types/api/licensesV3/getActive';
import { OrgPreference } from 'types/api/preferences/preference'; import {
OrgPreference,
UserPreference,
} from 'types/api/preferences/preference';
import { Organization } from 'types/api/user/getOrganization'; import { Organization } from 'types/api/user/getOrganization';
import { USER_ROLES } from 'types/roles'; import { USER_ROLES } from 'types/roles';
@ -45,6 +49,11 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
const [orgPreferences, setOrgPreferences] = useState<OrgPreference[] | null>( const [orgPreferences, setOrgPreferences] = useState<OrgPreference[] | null>(
null, null,
); );
const [userPreferences, setUserPreferences] = useState<
UserPreference[] | null
>(null);
const [isLoggedIn, setIsLoggedIn] = useState<boolean>( const [isLoggedIn, setIsLoggedIn] = useState<boolean>(
(): boolean => getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true', (): boolean => getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true',
); );
@ -168,6 +177,26 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
} }
}, [orgPreferencesData, isFetchingOrgPreferences]); }, [orgPreferencesData, isFetchingOrgPreferences]);
// now since org preferences data is dependent on user being loaded as well so we added extra safety net for user.email to be set as well
const {
data: userPreferencesData,
isFetching: isFetchingUserPreferences,
} = useQuery({
queryFn: () => listUserPreferences(),
queryKey: ['getAllUserPreferences', 'app-context'],
enabled: !!isLoggedIn && !!user.email,
});
useEffect(() => {
if (
userPreferencesData &&
userPreferencesData.data &&
!isFetchingUserPreferences
) {
setUserPreferences(userPreferencesData.data);
}
}, [userPreferencesData, isFetchingUserPreferences, isLoggedIn]);
function updateUser(user: IUser): void { function updateUser(user: IUser): void {
setUser((prev) => ({ setUser((prev) => ({
...prev, ...prev,
@ -175,6 +204,23 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
})); }));
} }
const updateUserPreferenceInContext = useCallback(
(userPreference: UserPreference): void => {
setUserPreferences((prev) => {
const index = prev?.findIndex((e) => e.name === userPreference.name);
if (index !== undefined) {
return [
...(prev?.slice(0, index) || []),
userPreference,
...(prev?.slice(index + 1, prev.length) || []),
];
}
return prev;
});
},
[],
);
function updateOrgPreferences(orgPreferences: OrgPreference[]): void { function updateOrgPreferences(orgPreferences: OrgPreference[]): void {
setOrgPreferences(orgPreferences); setOrgPreferences(orgPreferences);
} }
@ -235,7 +281,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
const value: IAppContext = useMemo( const value: IAppContext = useMemo(
() => ({ () => ({
user, user,
activeLicense, userPreferences,
featureFlags, featureFlags,
trialInfo, trialInfo,
orgPreferences, orgPreferences,
@ -249,9 +295,11 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
activeLicenseFetchError, activeLicenseFetchError,
featureFlagsFetchError, featureFlagsFetchError,
orgPreferencesFetchError, orgPreferencesFetchError,
activeLicense,
activeLicenseRefetch, activeLicenseRefetch,
updateUser, updateUser,
updateOrgPreferences, updateOrgPreferences,
updateUserPreferenceInContext,
updateOrg, updateOrg,
versionData: versionData?.payload || null, versionData: versionData?.payload || null,
}), }),
@ -259,6 +307,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
trialInfo, trialInfo,
activeLicense, activeLicense,
activeLicenseFetchError, activeLicenseFetchError,
userPreferences,
featureFlags, featureFlags,
featureFlagsFetchError, featureFlagsFetchError,
isFetchingActiveLicense, isFetchingActiveLicense,
@ -268,8 +317,9 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
isLoggedIn, isLoggedIn,
org, org,
orgPreferences, orgPreferences,
orgPreferencesFetchError,
activeLicenseRefetch, activeLicenseRefetch,
orgPreferencesFetchError,
updateUserPreferenceInContext,
updateOrg, updateOrg,
user, user,
userFetchError, userFetchError,

View File

@ -1,7 +1,10 @@
import APIError from 'types/api/error'; import APIError from 'types/api/error';
import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeaturesFlags'; import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeaturesFlags';
import { LicenseResModel, TrialInfo } from 'types/api/licensesV3/getActive'; import { LicenseResModel, TrialInfo } from 'types/api/licensesV3/getActive';
import { OrgPreference } from 'types/api/preferences/preference'; import {
OrgPreference,
UserPreference,
} from 'types/api/preferences/preference';
import { Organization } from 'types/api/user/getOrganization'; import { Organization } from 'types/api/user/getOrganization';
import { UserResponse as User } from 'types/api/user/getUser'; import { UserResponse as User } from 'types/api/user/getUser';
import { PayloadProps } from 'types/api/user/getVersion'; import { PayloadProps } from 'types/api/user/getVersion';
@ -12,6 +15,7 @@ export interface IAppContext {
trialInfo: TrialInfo | null; trialInfo: TrialInfo | null;
featureFlags: FeatureFlags[] | null; featureFlags: FeatureFlags[] | null;
orgPreferences: OrgPreference[] | null; orgPreferences: OrgPreference[] | null;
userPreferences: UserPreference[] | null;
isLoggedIn: boolean; isLoggedIn: boolean;
org: Organization[] | null; org: Organization[] | null;
isFetchingUser: boolean; isFetchingUser: boolean;
@ -25,6 +29,7 @@ export interface IAppContext {
activeLicenseRefetch: () => void; activeLicenseRefetch: () => void;
updateUser: (user: IUser) => void; updateUser: (user: IUser) => void;
updateOrgPreferences: (orgPreferences: OrgPreference[]) => void; updateOrgPreferences: (orgPreferences: OrgPreference[]) => void;
updateUserPreferenceInContext: (userPreference: UserPreference) => void;
updateOrg(orgId: string, updatedOrgName: string): void; updateOrg(orgId: string, updatedOrgName: string): void;
versionData: PayloadProps | null; versionData: PayloadProps | null;
} }

View File

@ -671,37 +671,14 @@ notifications - 2050
*/ */
@font-face { /* Import fonts from CDN */
font-family: 'Geist Mono'; @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
src: local('Geist Mono'), @import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@500&display=swap');
url('../public/fonts/GeistMonoVF.woff2') format('woff'); @import url('https://fonts.googleapis.com/css2?family=Space+Mono&display=swap');
/* Add other formats if needed (e.g., woff2, truetype, opentype, svg) */ @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap');
} @import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100;200;300;400;500;600;700;800;900&display=swap');
@font-face { /* Remove the old Geist Mono font-face declarations since we're using Google Fonts */
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-face {
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;
}
@keyframes spin { @keyframes spin {
from { from {
@ -742,3 +719,19 @@ notifications - 2050
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
} }
.metrics-application-container,
.service-metric-table-container,
.service-traces-table-container {
padding: 0px 8px;
}
.ap-dex-settings-popover-content {
display: flex;
width: 100%;
justify-content: flex-end;
}
.service-map-container {
padding: 0px 8px;
}

View File

@ -1,6 +1,7 @@
/* eslint-disable sonarjs/no-duplicate-string */ /* eslint-disable sonarjs/no-duplicate-string */
import { render, RenderOptions, RenderResult } from '@testing-library/react'; import { render, RenderOptions, RenderResult } from '@testing-library/react';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { ResourceProvider } from 'hooks/useResourceAttribute'; import { ResourceProvider } from 'hooks/useResourceAttribute';
import { AppContext } from 'providers/App/App'; import { AppContext } from 'providers/App/App';
@ -217,7 +218,7 @@ export function getAppContextMock(
featureFlagsFetchError: null, featureFlagsFetchError: null,
orgPreferences: [ orgPreferences: [
{ {
name: 'org_onboarding', name: ORG_PREFERENCES.ORG_ONBOARDING,
description: 'Organisation Onboarding', description: 'Organisation Onboarding',
valueType: 'boolean', valueType: 'boolean',
defaultValue: false, defaultValue: false,
@ -226,6 +227,8 @@ export function getAppContextMock(
value: false, value: false,
}, },
], ],
userPreferences: [],
updateUserPreferenceInContext: jest.fn(),
isFetchingOrgPreferences: false, isFetchingOrgPreferences: false,
orgPreferencesFetchError: null, orgPreferencesFetchError: null,
isLoggedIn: true, isLoggedIn: true,

View File

@ -2,18 +2,18 @@ export interface OrgPreference {
name: string; name: string;
description: string; description: string;
valueType: string; valueType: string;
defaultValue: boolean; defaultValue: unknown;
allowedValues: string[]; allowedValues: string[];
allowedScopes: string[]; allowedScopes: string[];
value: boolean; value: unknown;
} }
export interface UserPreference { export interface UserPreference {
name: string; name: string;
description: string; description: string;
valueType: string; valueType: string;
defaultValue: boolean; defaultValue: unknown;
allowedValues: string[]; allowedValues: string[];
allowedScopes: string[]; allowedScopes: string[];
value: boolean; value: unknown;
} }