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",
"sending_channels_note": "The alerts will be sent to all the configured channels.",
"loading_channels_message": "Loading Channels..",
"page_title_create": "New Notification Channels",
"page_title_edit": "Edit Notification Channels",
"page_title_create": "New Notification Channel",
"page_title_edit": "Edit Notification Channel",
"button_save_channel": "Save",
"button_test_channel": "Test",
"button_return": "Back",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,12 @@
import { Tabs, TabsProps } from 'antd';
import { useLocation, useParams } from 'react-router-dom';
import { RouteTabProps } from './types';
interface Params {
[key: string]: string;
}
function RouteTab({
routes,
activeKey,
@ -9,19 +14,38 @@ function RouteTab({
history,
...rest
}: 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 => {
if (onChangeHandler) {
onChangeHandler(activeRoute);
}
const selectedRoute = routes.find((e) => e.key === activeRoute);
const selectedRoute = routesWithParams.find((e) => e.key === activeRoute);
if (selectedRoute) {
history.push(selectedRoute.route);
}
};
const items = routes.map(({ Component, name, route, key }) => ({
const items = routesWithParams.map(({ Component, name, route, key }) => ({
label: name,
key,
tabKey: route,
@ -32,8 +56,8 @@ function RouteTab({
<Tabs
onChange={onChange}
destroyInactiveTabPane
activeKey={activeKey}
defaultActiveKey={activeKey}
activeKey={currentRoute?.key || activeKey}
defaultActiveKey={currentRoute?.key || activeKey}
animated
items={items}
// 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',
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/settings/channels/new',
CHANNELS_EDIT: '/settings/channels/:id',
CHANNELS_EDIT: '/settings/channels/edit/:id',
ALL_ERROR: '/exceptions',
ERROR_DETAIL: '/error-detail',
VERSION: '/status',
MY_SETTINGS: '/my-settings',
SETTINGS: '/settings',
MY_SETTINGS: '/settings/my-settings',
ORG_SETTINGS: '/settings/org-settings',
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
API_KEYS: '/settings/api-keys',
@ -52,7 +52,7 @@ const ROUTES = {
LIST_LICENSES: '/licenses',
LOGS_INDEX_FIELDS: '/logs-explorer/index-fields',
TRACE_EXPLORER: '/trace-explorer',
BILLING: '/billing',
BILLING: '/settings/billing',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views',
@ -60,7 +60,7 @@ const ROUTES = {
TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId',
WORKSPACE_LOCKED: '/workspace-locked',
WORKSPACE_SUSPENDED: '/workspace-suspended',
SHORTCUTS: '/shortcuts',
SHORTCUTS: '/settings/shortcuts',
INTEGRATIONS: '/integrations',
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
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 onClickEditHandler = useCallback((id: string) => {
history.replace(
history.push(
generatePath(ROUTES.CHANNELS_EDIT, {
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 { Tooltip, Typography } from 'antd';
import getAll from 'api/channels/getAll';
@ -56,7 +58,7 @@ function AlertChannels(): JSX.Element {
}
return (
<>
<div className="alert-channels-container">
<ButtonContainer>
<Paragraph ellipsis type="secondary">
{t('sending_channels_note')}
@ -87,7 +89,7 @@ function AlertChannels(): JSX.Element {
</ButtonContainer>
<AlertChannelsComponent allChannels={data?.data || []} />
</>
</div>
);
}

View File

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

View File

@ -18,6 +18,7 @@ import { Events } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import { USER_PREFERENCES } from 'constants/userPreferences';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
import dayjs from 'dayjs';
@ -27,7 +28,6 @@ import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
import { useAppContext } from 'providers/App/App';
import {
ReactNode,
@ -41,7 +41,7 @@ import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import { useMutation, useQueries } from 'react-query';
import { useDispatch } from 'react-redux';
import { matchPath, useLocation } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import {
@ -80,6 +80,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
userPreferences,
} = useAppContext();
const { notifications } = useNotifications();
@ -330,53 +331,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
});
}, [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(() => {
if (isDarkMode) {
document.body.classList.remove('lightMode');
@ -593,6 +547,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div>
);
const sideNavPinned = userPreferences?.find(
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
)?.value as boolean;
return (
<Layout className={cx(isDarkMode ? 'darkMode dark' : 'lightMode')}>
<Helmet>
@ -645,9 +603,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)}
<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
className={cx('app-content', {
'full-screen-content': renderFullScreen,
@ -657,32 +621,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent data-overlayscrollbars-initialize>
<OverlayScrollbar>
<ChildrenContainer
style={{
margin:
isHome() ||
isLogsView() ||
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView() ||
isAlertHistory() ||
isAlertOverview() ||
isMessagingQueues() ||
isCloudIntegrationPage() ||
isInfraMonitoring() ||
isApiMonitoringView() ||
isExceptionsView()
? 0
: '0 1rem',
...(isTraceDetailsView() ||
isTracesFunnels() ||
isTracesFunnelDetails()
? { margin: 0 }
: {}),
}}
>
<ChildrenContainer>
{isToDisplayLayout && !renderFullScreen && <TopNav />}
{children}
</ChildrenContainer>

View File

@ -1,7 +1,8 @@
.billing-container {
margin-bottom: 40px;
padding-top: 36px;
width: 65%;
width: 90%;
margin: 0 auto;
.billing-summary {
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 createEmail from 'api/channels/createEmail';
import createMsTeamsApi from 'api/channels/createMsTeams';
@ -477,26 +479,28 @@ function CreateAlertChannels({
);
return (
<FormAlertChannels
{...{
formInstance,
onTypeChangeHandler,
setSelectedConfig,
type,
onTestHandler,
onSaveHandler,
savingState,
testingState,
title: t('page_title_create'),
initialValue: {
<div className="create-alert-channels-container">
<FormAlertChannels
{...{
formInstance,
onTypeChangeHandler,
setSelectedConfig,
type,
...selectedConfig,
...PagerInitialConfig,
...OpsgenieInitialConfig,
...EmailInitialConfig,
},
}}
/>
onTestHandler,
onSaveHandler,
savingState,
testingState,
title: t('page_title_create'),
initialValue: {
type,
...selectedConfig,
...PagerInitialConfig,
...OpsgenieInitialConfig,
...EmailInitialConfig,
},
}}
/>
</div>
);
}

View File

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

View File

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

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 { Wrench } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useTranslation } from 'react-i18next';
@ -13,16 +15,19 @@ function Licenses(): JSX.Element {
return <Spinner tip={t('loading_licenses')} height="90vh" />;
}
const tabs = [
{
label: t('tab_current_license'),
key: 'licenses',
children: <ApplyLicenseForm licenseRefetch={activeLicenseRefetch} />,
},
];
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`
&&& {
padding-top: 1em;
padding-bottom: 1em;
padding: 16px;
}
`;

View File

@ -1,3 +1,12 @@
.my-settings-container {
display: flex;
flex-direction: column;
gap: 48px;
width: 80%;
margin: 12px auto;
}
.flexBtn {
display: flex;
align-items: center;
@ -8,4 +17,163 @@
display: flex;
align-items: center;
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;
return (
<Card>
<Card className="reset-password-card">
<Space direction="vertical" size="small">
<Typography.Title
level={4}

View File

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

View File

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

View File

@ -5,3 +5,231 @@
.userInfo-value {
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 './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 { 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 { useState } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import APIError from 'types/api/error';
import { NameInput } from '../styles';
function UserInfo(): JSX.Element {
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>(
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 />;
}
const onClickUpdateHandler = async (): Promise<void> => {
const hideUpdateNameModal = (): void => {
setIsUpdateNameModalOpen(false);
};
const hideResetPasswordModal = (): void => {
setIsResetPasswordModalOpen(false);
};
const onChangePasswordClickHandler = async (): Promise<void> => {
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({
displayName: changedName,
userId: user.id,
@ -44,80 +124,143 @@ function UserInfo(): JSX.Element {
...user,
displayName: changedName,
});
setLoading(false);
setIsLoading(false);
hideUpdateNameModal();
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
}
setLoading(false);
setIsLoading(false);
};
if (!user || !org) {
return <div />;
}
return (
<Card>
<Space direction="vertical" size="middle">
<Flex gap={8}>
<Typography.Title level={4} style={{ marginTop: 0 }}>
User Details
</Typography.Title>
</Flex>
<div className="user-info-card">
<div className="user-info">
<div className="user-name">{user.displayName}</div>
<Flex gap={16}>
<Space>
<Typography className="userInfo-label" data-testid="name-label">
Name
</Typography>
<NameInput
data-testid="name-textbox"
placeholder="Your Name"
onChange={(event): void => {
setChangedName(event.target.value);
}}
value={changedName}
disabled={loading}
/>
</Space>
<div className="user-info-subsection">
<div className="user-email">
<MailIcon size={16} /> {user.email}
</div>
<div className="user-role">
<UserIcon size={16} /> {user.role.toLowerCase()}
</div>
</div>
</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
type="default"
className="periscope-btn secondary"
icon={<FileTerminal size={16} />}
onClick={(): void => setIsResetPasswordModalOpen(true)}
>
Reset password
</Button>
</div>
<Modal
className="update-name-modal"
title={<span className="title">Update name</span>}
open={isUpdateNameModalOpen}
closable
onCancel={hideUpdateNameModal}
footer={[
<Button
className="flexBtn"
loading={loading}
disabled={loading}
onClick={onClickUpdateHandler}
data-testid="update-name-button"
key="submit"
type="primary"
icon={<Check size={16} />}
onClick={onSaveHandler}
disabled={isLoading}
data-testid="update-name-btn"
>
<PencilIcon size={12} /> Update
</Button>
</Flex>
<Space>
<Typography className="userInfo-label" data-testid="email-label">
{' '}
Email{' '}
</Typography>
Update name
</Button>,
]}
>
<Typography.Text>Name</Typography.Text>
<div className="update-name-input">
<Input
className="userInfo-value"
data-testid="email-textbox"
value={user.email}
disabled
placeholder="e.g. John Doe"
value={changedName}
onChange={(e): void => setChangedName(e.target.value)}
/>
</Space>
</div>
</Modal>
<Space>
<Typography className="userInfo-label" data-testid="role-label">
{' '}
Role{' '}
</Typography>
<Input
className="userInfo-value"
value={user.role || ''}
disabled
data-testid="role-textbox"
/>
</Space>
</Space>
</Card>
<Modal
className="reset-password-modal"
title={<span className="title">Reset password</span>}
open={isResetPasswordModalOpen}
closable
onCancel={hideResetPasswordModal}
footer={[
<Button
key="submit"
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
/>
</div>
<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';
const toggleThemeFunction = jest.fn();
const logEventFunction = jest.fn();
jest.mock('hooks/useDarkMode', () => ({
__esModule: true,
useIsDarkMode: jest.fn(() => ({
toggleTheme: toggleThemeFunction,
})),
useIsDarkMode: jest.fn(() => true),
default: jest.fn(() => ({
toggleTheme: toggleThemeFunction,
})),
}));
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn((eventName, data) => logEventFunction(eventName, data)),
}));
const errorNotification = jest.fn();
const successNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({
@ -25,90 +29,97 @@ jest.mock('hooks/useNotifications', () => ({
})),
}));
enum ThemeOptions {
Dark = 'Dark',
Light = 'Light Beta',
}
const THEME_SELECTOR_TEST_ID = 'theme-selector';
const RESET_PASSWORD_BUTTON_TEXT = 'Reset password';
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', () => {
beforeEach(() => {
jest.clearAllMocks();
render(<MySettingsContainer />);
});
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();
const darkThemeIcon = screen.getByTestId('dark-theme-icon');
expect(darkThemeIcon).toBeInTheDocument();
expect(darkThemeIcon.tagName).toBe('svg');
// Check Light theme option
expect(screen.getByText('Light')).toBeInTheDocument();
const lightThemeIcon = screen.getByTestId('light-theme-icon');
expect(lightThemeIcon).toBeInTheDocument();
expect(lightThemeIcon.tagName).toBe('svg');
expect(screen.getByText('Beta')).toBeInTheDocument();
});
it('Should activate Dark and Light buttons on click', async () => {
const initialSelectedOption = screen.getByRole('radio', {
name: ThemeOptions.Dark,
});
expect(initialSelectedOption).toBeChecked();
const newThemeOption = screen.getByRole('radio', {
name: ThemeOptions.Light,
});
fireEvent.click(newThemeOption);
expect(newThemeOption).toBeChecked();
it('Should have Dark theme selected by default', async () => {
const themeSelector = screen.getByTestId(THEME_SELECTOR_TEST_ID);
const darkOption = themeSelector.querySelector(
'input[value="dark"]',
) as HTMLInputElement;
expect(darkOption).toBeChecked();
});
it('Should switch the them on clicking Light theme', async () => {
const lightThemeOption = screen.getByRole('radio', {
name: /light/i,
});
fireEvent.click(lightThemeOption);
it('Should switch theme and log event when Light theme is selected', async () => {
const themeSelector = screen.getByTestId(THEME_SELECTOR_TEST_ID);
const lightOption = themeSelector.querySelector(
'input[value="light"]',
) as HTMLInputElement;
fireEvent.click(lightOption);
await waitFor(() => {
expect(toggleThemeFunction).toBeCalled();
expect(toggleThemeFunction).toHaveBeenCalled();
expect(logEventFunction).toHaveBeenCalledWith(
'Account Settings: Theme Changed',
{
theme: 'light',
},
);
});
});
});
describe('User Details Form', () => {
it('Should properly display the User Details Form', () => {
const userDetailsHeader = screen.getByRole('heading', {
name: /user details/i,
});
const nameLabel = screen.getByTestId('name-label');
const nameTextbox = screen.getByTestId('name-textbox');
const updateNameButton = screen.getByTestId('update-name-button');
const emailLabel = screen.getByTestId('email-label');
const emailTextbox = screen.getByTestId('email-textbox');
const roleLabel = screen.getByTestId('role-label');
const roleTextbox = screen.getByTestId('role-textbox');
// Open the Update name modal first
const updateNameButton = screen.getByText(UPDATE_NAME_BUTTON_TEXT);
fireEvent.click(updateNameButton);
// Find the label with class 'ant-typography' and text 'Name'
const nameLabels = screen.getAllByText('Name');
const nameLabel = nameLabels.find((el) =>
el.className.includes('ant-typography'),
);
const nameTextbox = screen.getByPlaceholderText('e.g. John Doe');
const modalUpdateNameButton = screen.getByTestId(UPDATE_NAME_BUTTON_TEST_ID);
expect(userDetailsHeader).toBeInTheDocument();
expect(nameLabel).toBeInTheDocument();
expect(nameTextbox).toBeInTheDocument();
expect(updateNameButton).toBeInTheDocument();
expect(emailLabel).toBeInTheDocument();
expect(emailTextbox).toBeInTheDocument();
expect(roleLabel).toBeInTheDocument();
expect(roleTextbox).toBeInTheDocument();
expect(modalUpdateNameButton).toBeInTheDocument();
});
it('Should update the name on clicking Update button', async () => {
const nameTextbox = screen.getByTestId('name-textbox');
const updateNameButton = screen.getByTestId('update-name-button');
// Open the Update name modal first
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(() => {
fireEvent.change(nameTextbox, { target: { value: 'New Name' } });
});
fireEvent.click(updateNameButton);
fireEvent.click(modalUpdateNameButton);
await waitFor(() =>
expect(successNotification).toHaveBeenCalledWith({
@ -119,92 +130,53 @@ describe('MySettings Flows', () => {
});
describe('Reset password', () => {
let currentPasswordTextbox: Node | Window;
let newPasswordTextbox: Node | Window;
let submitButtonElement: HTMLElement;
it('Should open password reset modal when clicking Reset password button', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
// The first button is the one in the user info section
fireEvent.click(resetPasswordButtons[0]);
beforeEach(() => {
currentPasswordTextbox = screen.getByTestId('current-password-textbox');
newPasswordTextbox = screen.getByTestId('new-password-textbox');
submitButtonElement = screen.getByTestId('update-password-button');
});
it('Should properly display the Password Reset Form', () => {
const passwordResetHeader = screen.getByTestId('change-password-header');
expect(passwordResetHeader).toBeInTheDocument();
const currentPasswordLabel = screen.getByTestId('current-password-label');
expect(currentPasswordLabel).toBeInTheDocument();
expect(currentPasswordTextbox).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');
// Check if modal is opened (look for modal title)
expect(
screen.getByText((content, element) =>
Boolean(
element &&
'className' in element &&
typeof element.className === 'string' &&
element.className.includes('title') &&
content === RESET_PASSWORD_BUTTON_TEXT,
),
),
).toBeInTheDocument();
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toBeInTheDocument();
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toBeInTheDocument();
});
it('Should display validation error if password is less than 8 characters', async () => {
const currentPasswordTextbox = screen.getByTestId(
'current-password-textbox',
);
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
act(() => {
fireEvent.change(currentPasswordTextbox, { target: { value: '123' } });
});
const validationMessage = await screen.findByTestId('validation-message');
await waitFor(() => {
expect(validationMessage).toHaveTextContent(
'Password must a have minimum of 8 characters',
);
// Use getByTestId for the validation message (if present in your modal/component)
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 () => {
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123456879' },
});
it('Should disable reset button when current and new passwords are the same', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
fireEvent.change(newPasswordTextbox, { target: { value: '123456789' } });
});
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();
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, {
@ -213,7 +185,25 @@ describe('MySettings Flows', () => {
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 { Button, Radio, RadioChangeEvent, Space, Tag, Typography } from 'antd';
import { Logout } from 'api/utils';
import { Radio, RadioChangeEvent, Switch, Tag } from 'antd';
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 { LogOut, Moon, Sun } from 'lucide-react';
import { useState } from 'react';
import { useNotifications } from 'hooks/useNotifications';
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 UserInfo from './UserInfo';
function MySettings(): JSX.Element {
const isDarkMode = useIsDarkMode();
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 = [
{
@ -39,57 +73,112 @@ function MySettings(): JSX.Element {
const [theme, setTheme] = useState(isDarkMode ? 'dark' : 'light');
const handleThemeChange = ({ target: { value } }: RadioChangeEvent): void => {
logEvent('Account Settings: Theme Changed', {
theme: value,
});
setTheme(value);
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 (
<Space
direction="vertical"
size="large"
style={{
margin: '16px 0',
}}
>
<div className="theme-selector">
<Typography.Title
level={5}
style={{
margin: '0 0 16px 0',
}}
>
{' '}
Theme{' '}
</Typography.Title>
<Radio.Group
options={themeOptions}
onChange={handleThemeChange}
value={theme}
optionType="button"
buttonStyle="solid"
data-testid="theme-selector"
/>
<div className="my-settings-container">
<div className="user-info-section">
<div className="user-info-section-header">
<div className="user-info-section-title">General </div>
<div className="user-info-section-subtitle">
Manage your account settings.
</div>
</div>
<div className="user-info-container">
<UserInfo />
</div>
</div>
<div className="user-info-container">
<UserInfo />
<div className="user-preference-section">
<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
options={themeOptions}
onChange={handleThemeChange}
value={theme}
optionType="button"
buttonStyle="solid"
data-testid="theme-selector"
size="small"
/>
</div>
<div className="user-preference-section-content-item-description">
Select if SigNoz&apos;s appearance should be light or dark
</div>
</div>
<TimezoneAdaptation />
<div className="user-preference-section-content-item">
<div className="user-preference-section-content-item-title-action">
Keep the primary sidebar always open{' '}
<Switch
checked={sideNavPinned}
onChange={handleSideNavPinnedChange}
loading={isUpdatingUserPreference}
/>
</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 className="password-reset-container">
<Password />
</div>
<TimezoneAdaptation />
<Button
className="flexBtn"
onClick={(): void => Logout()}
type="primary"
data-testid="logout-button"
>
<LogOut size={12} /> Logout
</Button>
</Space>
</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> = {
[ROUTES.LOGS_EXPLORER]: 'Switch to Old Logs Explorer',
[ROUTES.TRACE]: 'Try new Traces Explorer',
[ROUTES.OLD_LOGS_EXPLORER]: 'Switch to New Logs Explorer',
[ROUTES.TRACES_EXPLORER]: 'Switch to Old Trace Explorer',
[ROUTES.LOGS_EXPLORER]: 'Old Explorer',
[ROUTES.TRACE]: 'New Explorer',
[ROUTES.OLD_LOGS_EXPLORER]: 'New 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 ROUTES from 'constants/routes';
import history from 'lib/history';
import { Undo } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
@ -34,11 +36,11 @@ function NewExplorerCTA(): JSX.Element | null {
const button = useMemo(
() => (
<Button
icon={<CompassOutlined />}
icon={<Undo size={16} />}
onClick={onClickHandler}
danger
data-testid="newExplorerCTA"
type="primary"
type="text"
className="periscope-btn link"
>
{buttonText[location.pathname]}
</Button>

View File

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

View File

@ -298,8 +298,6 @@
}
.onboarding-v2 {
margin: 0px -1rem;
.onboarding-header-container {
display: flex;
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 (
<>
<div className="organization-settings-container">
<Space direction="vertical">
{org.map((e, index) => (
<DisplayName key={e.id} id={e.id} index={index} />
))}
</Space>
<Divider />
<PendingInvitesContainer />
<Divider />
<Members />
<Divider />
<AuthDomains />
</>
</div>
);
}

View File

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

View File

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

View File

@ -5,13 +5,13 @@
flex-direction: row;
align-items: center;
height: 36px;
height: 32px;
margin-bottom: 4px;
cursor: pointer;
&.active {
.nav-item-active-marker {
background: #3f5ecc;
background: #4e74f8;
}
}
@ -27,24 +27,24 @@
.nav-item-data {
color: white;
background: #121317;
background: var(--Slate-500, #161922);
}
}
&.active {
.nav-item-data {
color: white;
background: #121317;
background: var(--Slate-500, #161922);
// color: #3f5ecc;
}
}
.nav-item-active-marker {
margin: 8px 0;
margin: 4px 0;
width: 8px;
height: 24px;
background: transparent;
border-radius: 3px;
border-radius: 2px;
margin-left: -5px;
}
@ -53,24 +53,25 @@
max-width: calc(100% - 24px);
display: flex;
margin: 0px 8px;
padding: 4px 12px;
padding: 2px 8px;
flex-direction: row;
align-items: center;
gap: 8px;
align-self: stretch;
color: #c0c1c3;
border-radius: 3px;
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
font-weight: 300;
line-height: 18px;
background: transparent;
transition: 0.2s all linear;
border-radius: 3px;
.nav-item-icon {
height: 16px;
}
@ -80,6 +81,31 @@
white-space: nowrap;
overflow: hidden;
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 {
&.active {
.nav-item-active-marker {
background: #3f5ecc;
background: #4e74f8;
}
}
@ -115,6 +141,10 @@
.nav-item-data {
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 cx from 'classnames';
import { Pin, PinOff } from 'lucide-react';
import { SidebarItem } from '../sideNav.types';
@ -12,14 +13,27 @@ export default function NavItem({
isActive,
onClick,
isDisabled,
onTogglePin,
isPinned,
showIcon,
}: {
item: SidebarItem;
isActive: boolean;
onClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
isDisabled: boolean;
onTogglePin?: (item: SidebarItem) => void;
isPinned?: boolean;
showIcon?: boolean;
}): JSX.Element {
const { label, icon, isBeta, isNew } = item;
const handleTogglePinClick = (
event: React.MouseEvent<SVGSVGElement, MouseEvent>,
): void => {
event.stopPropagation();
onTogglePin?.(item);
};
return (
<div
className={cx(
@ -34,15 +48,15 @@ export default function NavItem({
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="nav-item-icon">{icon}</div>
{showIcon && <div className="nav-item-icon">{icon}</div>}
<div className="nav-item-label">{label}</div>
{isBeta && (
<div className="nav-item-beta">
<Tag bordered={false} color="geekblue">
<Tag bordered={false} className="sidenav-beta-tag">
Beta
</Tag>
</div>
@ -55,7 +69,31 @@ export default function NavItem({
</Tag>
</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>
);
}
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,
BellDot,
Binoculars,
Book,
Boxes,
BugIcon,
Cloudy,
DraftingCompass,
FileKey2,
Github,
Globe,
HardDrive,
Home,
Key,
Keyboard,
Layers2,
LayoutGrid,
ListMinus,
MessageSquare,
MessageSquareText,
Plus,
Receipt,
Route,
ScrollText,
Settings,
Slack,
Unplug,
// Unplug,
User,
UserPlus,
} from 'lucide-react';
@ -60,11 +66,12 @@ export const manageLicenseMenuItem = {
export const helpSupportMenuItem = {
key: ROUTES.SUPPORT,
label: 'Help & Support',
icon: <MessageSquare size={16} />,
icon: <MessageSquareText size={16} />,
};
export const shortcutMenuItem = {
key: ROUTES.SHORTCUTS,
// eslint-disable-next-line sonarjs/no-duplicate-string
label: 'Keyboard Shortcuts',
icon: <Layers2 size={16} />,
};
@ -86,79 +93,307 @@ const menuItems: SidebarItem[] = [
key: ROUTES.HOME,
label: 'Home',
icon: <Home size={16} />,
itemKey: 'home',
},
{
key: ROUTES.APPLICATION,
label: 'Services',
icon: <HardDrive size={16} />,
itemKey: 'services',
},
{
key: ROUTES.TRACES_EXPLORER,
label: 'Traces',
icon: <DraftingCompass size={16} />,
},
{
key: ROUTES.LOGS,
label: 'Logs',
icon: <ScrollText size={16} />,
itemKey: 'logs',
},
{
key: ROUTES.METRICS_EXPLORER,
label: 'Metrics',
icon: <BarChart2 size={16} />,
isNew: true,
itemKey: 'metrics',
},
{
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
label: 'Infra Monitoring',
icon: <Boxes size={16} />,
itemKey: 'infrastructure',
},
{
key: ROUTES.ALL_DASHBOARD,
label: 'Dashboards',
icon: <LayoutGrid size={16} />,
itemKey: 'dashboards',
},
{
key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
label: 'Messaging Queues',
icon: <ListMinus size={16} />,
itemKey: 'messaging-queues',
},
{
key: ROUTES.API_MONITORING,
label: 'External APIs',
icon: <Binoculars size={16} />,
isNew: true,
itemKey: 'external-apis',
},
{
key: ROUTES.LIST_ALL_ALERT,
label: 'Alerts',
icon: <BellDot size={16} />,
itemKey: 'alerts',
},
{
key: ROUTES.INTEGRATIONS,
label: 'Integrations',
icon: <Unplug size={16} />,
itemKey: 'integrations',
},
{
key: ROUTES.ALL_ERROR,
label: 'Exceptions',
icon: <BugIcon size={16} />,
itemKey: 'exceptions',
},
{
key: ROUTES.SERVICE_MAP,
label: 'Service Map',
icon: <Route size={16} />,
isBeta: true,
itemKey: 'service-map',
},
{
key: ROUTES.BILLING,
label: 'Billing',
icon: <Receipt size={16} />,
itemKey: 'billing',
},
{
key: ROUTES.SETTINGS,
label: 'Settings',
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 {
key: string | number;
icon?: ReactNode;
text?: ReactNode;
key: string | number;
label?: ReactNode;
isBeta?: boolean;
isNew?: boolean;
isPinned?: boolean;
children?: SidebarItem[];
isExternal?: boolean;
url?: string;
isEnabled?: boolean;
itemKey?: string;
}
export enum SecondaryMenuItemKey {

View File

@ -231,6 +231,9 @@ export const routesToSkip = [
ROUTES.CHANNELS_EDIT,
ROUTES.WORKSPACE_ACCESS_RESTRICTED,
ROUTES.ALL_ERROR,
ROUTES.UN_AUTHORIZED,
ROUTES.NOT_FOUND,
ROUTES.SOMETHING_WENT_WRONG,
];
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 ROUTES from 'constants/routes';
import { useMemo } from 'react';
@ -43,7 +45,7 @@ function TopNav(): JSX.Element | null {
}
return !isRouteToSkip ? (
<Row style={{ marginBottom: '1rem' }}>
<div className="top-nav-container">
<Col span={24} style={{ marginTop: '1rem' }}>
<Row justify="end">
<Space align="center" size={16} direction="horizontal">
@ -54,7 +56,7 @@ function TopNav(): JSX.Element | null {
</Space>
</Row>
</Col>
</Row>
</div>
) : 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 { Button, Card, Form, Space, Typography } from 'antd';
import './Version.styles.scss';
import { Button, Form } from 'antd';
import { CheckCircle, CloudUpload, InfoIcon, Wrench } from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
@ -34,73 +36,82 @@ function Version(): JSX.Element {
);
return (
<Card style={{ margin: '16px 0' }}>
<Typography.Title ellipsis level={4} style={{ marginTop: 0 }}>
{t('version')}
</Typography.Title>
<Form
wrapperCol={{
span: 14,
}}
labelCol={{
span: 3,
}}
layout="horizontal"
form={form}
labelAlign="left"
>
<Form.Item label={t('current_version')}>
<InputComponent
readOnly
value={isCurrentVersionError ? t('n_a').toString() : currentVersion}
placeholder={t('current_version')}
/>
</Form.Item>
<Form.Item label={t('latest_version')}>
<InputComponent
readOnly
value={isLatestVersionError ? t('n_a').toString() : latestVersion}
placeholder={t('latest_version')}
/>
<Button href={latestVersionUrl} target="_blank" type="link">
{t('release_notes')}
</Button>
</Form.Item>
</Form>
{!isError && isLatestVersion && (
<div>
<Space align="start">
<span></span>
<Typography.Paragraph italic>
{t('latest_version_signoz')}
</Typography.Paragraph>
</Space>
<div className="version-container">
<header className="version-page-header">
<div className="version-page-header-title">
<Wrench size={16} />
Version
</div>
)}
</header>
{!isError && !isLatestVersion && (
<div>
<Space align="start">
<span>
<WarningFilled style={{ color: '#E87040' }} />
</span>
<Typography.Paragraph italic>{t('stale_version')}</Typography.Paragraph>
</Space>
<div className="version-page-container">
<div className="version-card">
<Form
wrapperCol={{
span: 14,
}}
labelCol={{
span: 3,
}}
layout="horizontal"
form={form}
labelAlign="left"
>
<Form.Item label={t('current_version')}>
<InputComponent
readOnly
value={isCurrentVersionError ? t('n_a').toString() : currentVersion}
placeholder={t('current_version')}
/>
</Form.Item>
<Form.Item label={t('latest_version')}>
<InputComponent
readOnly
value={isLatestVersionError ? t('n_a').toString() : latestVersion}
placeholder={t('latest_version')}
/>
<Button href={latestVersionUrl} target="_blank" type="link">
{t('release_notes')}
</Button>
</Form.Item>
</Form>
{!isError && isLatestVersion && (
<div className="version-page-latest-version-container">
<div className="version-page-latest-version-container-title">
<CheckCircle size={16} />
{t('latest_version_signoz')}
</div>
</div>
)}
{!isError && !isLatestVersion && (
<div className="version-page-stale-version-container">
<div className="version-page-stale-version-container-title">
<InfoIcon size={16} />
{t('stale_version')}
</div>
</div>
)}
{!isError && !isLatestVersion && (
<div className="version-page-upgrade-container">
<Button
href="https://signoz.io/docs/operate/docker-standalone/#upgrade"
target="_blank"
type="primary"
className="periscope-btn primary"
icon={<CloudUpload size={16} />}
>
{t('read_how_to_upgrade')}
</Button>
</div>
)}
</div>
)}
{!isError && !isLatestVersion && (
<Button
href="https://signoz.io/docs/operate/docker-standalone/#upgrade"
target="_blank"
>
{t('read_how_to_upgrade')}
</Button>
)}
</Card>
</div>
</div>
);
}

View File

@ -9,6 +9,11 @@
<meta http-equiv="Pragma" content="no-cache" />
<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">
Open source Observability platform | SigNoz
</title>
@ -53,6 +58,17 @@
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
<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>
<body>
<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 { HelmetProvider } from 'react-helmet-async';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import { Provider } from 'react-redux';
import store from 'store';
@ -47,9 +46,6 @@ if (container) {
<AppRoutes />
</AppProvider>
</Provider>
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
</TimezoneProvider>
</ThemeProvider>

View File

@ -95,7 +95,7 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
);
}
return (
<Container>
<div className="service-map-container">
<ResourceAttributesFilter
suffixIcon={
<TextToolTip
@ -109,7 +109,7 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
/>
<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 { TabsProps } from 'antd/lib';
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
@ -70,7 +72,7 @@ function AllAlertList(): JSX.Element {
}
safeNavigate(`/alerts?${params}`);
}}
className={`${
className={`alerts-container ${
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 */
import './ChannelsEdit.styles.scss';
import { Typography } from 'antd';
import get from 'api/channels/get';
import Spinner from 'components/Spinner';
@ -128,15 +131,17 @@ function ChannelsEdit(): JSX.Element {
const target = prepChannelConfig();
return (
<EditAlertChannels
{...{
initialValue: {
...target.channel,
type: target.type,
name: value.name,
},
}}
/>
<div className="edit-alert-channels-container">
<EditAlertChannels
{...{
initialValue: {
...target.channel,
type: target.type,
name: value.name,
},
}}
/>
</div>
);
}
interface Params {

View File

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

View File

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

View File

@ -138,9 +138,9 @@ describe('Logs Explorer Tests', () => {
expect(timeSeriesView).toBeInTheDocument();
expect(tableView).toBeInTheDocument();
// check the presence of old logs explorer CTA
const oldLogsCTA = getByText('Switch to Old Logs Explorer');
expect(oldLogsCTA).toBeInTheDocument();
// // 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');
// expect(oldLogsCTA).toBeInTheDocument();
});
// update this test properly

View File

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

View File

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

View File

@ -19,6 +19,7 @@
.ant-tabs-content-holder {
display: flex;
padding: 16px;
.ant-tabs-content {
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 AlertChannels from 'container/AllAlertChannels';
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 GeneralSettings from 'container/GeneralSettings';
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
import IngestionSettings from 'container/IngestionSettings/IngestionSettings';
import MultiIngestionSettings from 'container/IngestionSettings/MultiIngestionSettings';
import MySettings from 'container/MySettings';
import OrganizationSettings from 'container/OrganizationSettings';
import { TFunction } from 'i18next';
import {
@ -14,9 +18,16 @@ import {
BellDot,
Building,
Cpu,
CreditCard,
Globe,
Keyboard,
KeySquare,
Pencil,
Plus,
User,
} from 'lucide-react';
import ChannelsEdit from 'pages/ChannelsEdit';
import Shortcuts from 'pages/Shortcuts';
export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [
{
@ -123,3 +134,70 @@ export const customDomainSettings = (t: TFunction): RouteTabProps['routes'] => [
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 { 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} />;
}
import SettingsPage from './Settings';
export default SettingsPage;

View File

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

View File

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

View File

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

View File

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

View File

@ -11,8 +11,9 @@ function UnAuthorizePage(): JSX.Element {
<Typography.Title level={3}>
Oops.. you don&apos;t have permission to view this page
</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>
</Space>
</Container>

View File

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

View File

@ -1,6 +1,7 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import { Logout } from 'api/utils';
import listOrgPreferences from 'api/v1/org/preferences/list';
import listUserPreferences from 'api/v1/user/preferences/list';
import getUserVersion from 'api/v1/version/getVersion';
import { LOCALSTORAGE } from 'constants/localStorage';
import dayjs from 'dayjs';
@ -25,7 +26,10 @@ import {
LicenseState,
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 { USER_ROLES } from 'types/roles';
@ -45,6 +49,11 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
const [orgPreferences, setOrgPreferences] = useState<OrgPreference[] | null>(
null,
);
const [userPreferences, setUserPreferences] = useState<
UserPreference[] | null
>(null);
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(
(): boolean => getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true',
);
@ -168,6 +177,26 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
}
}, [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 {
setUser((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 {
setOrgPreferences(orgPreferences);
}
@ -235,7 +281,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
const value: IAppContext = useMemo(
() => ({
user,
activeLicense,
userPreferences,
featureFlags,
trialInfo,
orgPreferences,
@ -249,9 +295,11 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
activeLicenseFetchError,
featureFlagsFetchError,
orgPreferencesFetchError,
activeLicense,
activeLicenseRefetch,
updateUser,
updateOrgPreferences,
updateUserPreferenceInContext,
updateOrg,
versionData: versionData?.payload || null,
}),
@ -259,6 +307,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
trialInfo,
activeLicense,
activeLicenseFetchError,
userPreferences,
featureFlags,
featureFlagsFetchError,
isFetchingActiveLicense,
@ -268,8 +317,9 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
isLoggedIn,
org,
orgPreferences,
orgPreferencesFetchError,
activeLicenseRefetch,
orgPreferencesFetchError,
updateUserPreferenceInContext,
updateOrg,
user,
userFetchError,

View File

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

View File

@ -671,37 +671,14 @@ notifications - 2050
*/
@font-face {
font-family: 'Geist Mono';
src: local('Geist Mono'),
url('../public/fonts/GeistMonoVF.woff2') format('woff');
/* Add other formats if needed (e.g., woff2, truetype, opentype, svg) */
}
/* Import fonts from CDN */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@500&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Space+Mono&display=swap');
@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 {
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;
}
/* Remove the old Geist Mono font-face declarations since we're using Google Fonts */
@keyframes spin {
from {
@ -742,3 +719,19 @@ notifications - 2050
border: 1px solid #d1d5db;
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 */
import { render, RenderOptions, RenderResult } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import { AppContext } from 'providers/App/App';
@ -217,7 +218,7 @@ export function getAppContextMock(
featureFlagsFetchError: null,
orgPreferences: [
{
name: 'org_onboarding',
name: ORG_PREFERENCES.ORG_ONBOARDING,
description: 'Organisation Onboarding',
valueType: 'boolean',
defaultValue: false,
@ -226,6 +227,8 @@ export function getAppContextMock(
value: false,
},
],
userPreferences: [],
updateUserPreferenceInContext: jest.fn(),
isFetchingOrgPreferences: false,
orgPreferencesFetchError: null,
isLoggedIn: true,

View File

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