mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 23:47:12 +00:00
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:
parent
fff7f8fc76
commit
c477e0ef16
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
18
frontend/src/constants/orgPreferences.ts
Normal file
18
frontend/src/constants/orgPreferences.ts
Normal 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',
|
||||
};
|
||||
@ -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',
|
||||
|
||||
4
frontend/src/constants/userPreferences.ts
Normal file
4
frontend/src/constants/userPreferences.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const USER_PREFERENCES = {
|
||||
SIDENAV_PINNED: 'sidenav_pinned',
|
||||
NAV_SHORTCUTS: 'nav_shortcuts',
|
||||
};
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
.alert-channels-container {
|
||||
width: 90%;
|
||||
margin: 12px auto;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,12 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.side-nav-pinned {
|
||||
.app-content {
|
||||
width: calc(100% - 240px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-support-gateway {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
.billing-container {
|
||||
margin-bottom: 40px;
|
||||
padding-top: 36px;
|
||||
width: 65%;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
|
||||
.billing-summary {
|
||||
margin: 24px 8px;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@ -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 = {
|
||||
|
||||
91
frontend/src/container/Licenses/Licenses.styles.scss
Normal file
91
frontend/src/container/Licenses/Licenses.styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -3,8 +3,7 @@ import styled from 'styled-components';
|
||||
|
||||
export const ApplyFormContainer = styled.div`
|
||||
&&& {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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'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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@ -298,8 +298,6 @@
|
||||
}
|
||||
|
||||
.onboarding-v2 {
|
||||
margin: 0px -1rem;
|
||||
|
||||
.onboarding-header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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];
|
||||
|
||||
4
frontend/src/container/TopNav/TopNav.styles.scss
Normal file
4
frontend/src/container/TopNav/TopNav.styles.scss
Normal file
@ -0,0 +1,4 @@
|
||||
.top-nav-container {
|
||||
padding: 0px 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
122
frontend/src/container/Version/Version.styles.scss
Normal file
122
frontend/src/container/Version/Version.styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
10
frontend/src/pages/AlertList/AlertList.styles.scss
Normal file
10
frontend/src/pages/AlertList/AlertList.styles.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.alerts-container {
|
||||
.ant-tabs-nav-wrap {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
@ -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' : ''
|
||||
}`}
|
||||
/>
|
||||
|
||||
15
frontend/src/pages/ChannelsEdit/ChannelsEdit.styles.scss
Normal file
15
frontend/src/pages/ChannelsEdit/ChannelsEdit.styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -14,8 +14,9 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.request-entity-container {
|
||||
margin: 0;
|
||||
margin: 16px 0px;
|
||||
border-right: none;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -59,11 +59,11 @@ function MetricsApplication(): JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="metrics-application-container">
|
||||
<ResourceAttributesFilter />
|
||||
<ApDexApplication />
|
||||
<RouteTab routes={routes} history={history} activeKey={activeKey} />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
|
||||
104
frontend/src/pages/Settings/Settings.styles.scss
Normal file
104
frontend/src/pages/Settings/Settings.styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
234
frontend/src/pages/Settings/Settings.tsx
Normal file
234
frontend/src/pages/Settings/Settings.tsx
Normal 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;
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -11,8 +11,9 @@ function UnAuthorizePage(): JSX.Element {
|
||||
<Typography.Title level={3}>
|
||||
Oops.. you don'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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user