Compare commits

..

1 Commits

Author SHA1 Message Date
nityanandagohain
38b1d92252 fix: revert domain fixes 2025-06-12 22:38:16 +05:30
149 changed files with 1347 additions and 8995 deletions

View File

@@ -6,13 +6,11 @@ import (
"time"
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/analyticstypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
@@ -25,17 +23,16 @@ type provider struct {
config licensing.Config
settings factory.ScopedProviderSettings
orgGetter organization.Getter
analytics analytics.Analytics
stopChan chan struct{}
}
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
return New(ctx, providerSettings, config, store, zeus, orgGetter, analytics)
return New(ctx, providerSettings, config, store, zeus, orgGetter)
})
}
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) (licensing.Licensing, error) {
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) (licensing.Licensing, error) {
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
licensestore := sqllicensingstore.New(sqlstore)
return &provider{
@@ -45,7 +42,6 @@ func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Conf
settings: settings,
orgGetter: orgGetter,
stopChan: make(chan struct{}),
analytics: analytics,
}, nil
}
@@ -163,25 +159,6 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
stats := licensetypes.NewStatsFromLicense(activeLicense)
provider.analytics.Send(ctx,
analyticstypes.Track{
UserId: "stats_" + organizationID.String(),
Event: "License Updated",
Properties: analyticstypes.NewPropertiesFromMap(stats),
Context: &analyticstypes.Context{
Extra: map[string]interface{}{
analyticstypes.KeyGroupID: organizationID.String(),
},
},
},
analyticstypes.Group{
UserId: "stats_" + organizationID.String(),
GroupId: organizationID.String(),
Traits: analyticstypes.NewTraitsFromMap(stats),
},
)
return nil
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
"github.com/SigNoz/signoz/ee/zeus"
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/config"
"github.com/SigNoz/signoz/pkg/config/envprovider"
"github.com/SigNoz/signoz/pkg/config/fileprovider"
@@ -135,8 +134,8 @@ func main() {
zeus.Config(),
httpzeus.NewProviderFactory(),
licensing.Config(24*time.Hour, 3),
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter)
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),

View File

@@ -78,7 +78,7 @@
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"html-webpack-plugin": "5.5.0",
"http-proxy-middleware": "3.0.5",
"http-proxy-middleware": "3.0.3",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
@@ -250,7 +250,7 @@
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.5",
"http-proxy-middleware": "3.0.3",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ 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';
@@ -15,7 +14,6 @@ import { matchPath, useLocation } from 'react-router-dom';
import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
import { OrgPreference } from 'types/api/preferences/preference';
import { Organization } from 'types/api/user/getOrganization';
import { UserResponse } from 'types/api/user/getUser';
import { USER_ROLES } from 'types/roles';
@@ -97,8 +95,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
usersData.data
) {
const isOnboardingComplete = orgPreferences?.find(
(preference: OrgPreference) =>
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
(preference: Record<string, any>) => preference.name === 'org_onboarding',
)?.value;
const isFirstUser = checkFirstTimeUser();

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
@@ -53,32 +52,11 @@ jest.mock('hooks/saveViews/useDeleteView', () => ({
})),
}));
// Mock usePreferenceSync
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
formatting: {
maxLines: 2,
format: 'table',
fontSize: 'small',
version: 1,
},
},
loading: false,
error: null,
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
}),
}));
describe('ExplorerCard', () => {
it('renders a card with a title and a description', () => {
render(
<MockQueryClientProvider>
<PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</MockQueryClientProvider>,
);
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
@@ -87,9 +65,7 @@ describe('ExplorerCard', () => {
it('renders a save view button', () => {
render(
<MockQueryClientProvider>
<PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</MockQueryClientProvider>,
);
expect(screen.queryByText('Save view')).not.toBeInTheDocument();

View File

@@ -6,7 +6,6 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import isEqual from 'lodash-es/isEqual';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
DeleteViewHandlerProps,
@@ -107,11 +106,7 @@ export const isQueryUpdatedInView = ({
!isEqual(
options?.selectColumns,
extraData && JSON.parse(extraData)?.selectColumns,
) ||
(stagedQuery?.builder?.queryData?.[0]?.dataSource === DataSource.LOGS &&
(!isEqual(options?.format, extraData && JSON.parse(extraData)?.format) ||
!isEqual(options?.maxLines, extraData && JSON.parse(extraData)?.maxLines) ||
!isEqual(options?.fontSize, extraData && JSON.parse(extraData)?.fontSize)))
)
);
};

View File

@@ -74,7 +74,6 @@ const formatMap = {
'MM/dd HH:mm': DATE_TIME_FORMATS.SLASH_SHORT,
'MM/DD': DATE_TIME_FORMATS.DATE_SHORT,
'YY-MM': DATE_TIME_FORMATS.YEAR_MONTH,
'MMM d, yyyy, h:mm:ss aaaa': DATE_TIME_FORMATS.DASH_DATETIME,
YY: DATE_TIME_FORMATS.YEAR_SHORT,
};

View File

@@ -1,12 +1,7 @@
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,
@@ -14,38 +9,19 @@ 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 = routesWithParams.find((e) => e.key === activeRoute);
const selectedRoute = routes.find((e) => e.key === activeRoute);
if (selectedRoute) {
history.push(selectedRoute.route);
}
};
const items = routesWithParams.map(({ Component, name, route, key }) => ({
const items = routes.map(({ Component, name, route, key }) => ({
label: name,
key,
tabKey: route,
@@ -56,8 +32,8 @@ function RouteTab({
<Tabs
onChange={onChange}
destroyInactiveTabPane
activeKey={currentRoute?.key || activeKey}
defaultActiveKey={currentRoute?.key || activeKey}
activeKey={activeKey}
defaultActiveKey={activeKey}
animated
items={items}
// eslint-disable-next-line react/jsx-props-no-spreading

View File

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

View File

@@ -29,12 +29,12 @@ const ROUTES = {
ALERT_OVERVIEW: '/alerts/overview',
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/settings/channels/new',
CHANNELS_EDIT: '/settings/channels/edit/:id',
CHANNELS_EDIT: '/settings/channels/: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: '/settings/billing',
BILLING: '/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: '/settings/shortcuts',
SHORTCUTS: '/shortcuts',
INTEGRATIONS: '/integrations',
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,6 @@ 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';
@@ -28,6 +27,7 @@ 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 { useLocation } from 'react-router-dom';
import { matchPath, useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import {
@@ -80,7 +80,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
userPreferences,
} = useAppContext();
const { notifications } = useNotifications();
@@ -331,6 +330,53 @@ 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');
@@ -547,10 +593,6 @@ 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>
@@ -603,15 +645,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)}
<Flex
className={cx(
'app-layout',
isDarkMode ? 'darkMode dark' : 'lightMode',
sideNavPinned ? 'side-nav-pinned' : '',
)}
className={cx('app-layout', isDarkMode ? 'darkMode dark' : 'lightMode')}
>
{isToDisplayLayout && !renderFullScreen && (
<SideNav isPinned={sideNavPinned} />
)}
{isToDisplayLayout && !renderFullScreen && <SideNav />}
<div
className={cx('app-content', {
'full-screen-content': renderFullScreen,
@@ -621,7 +657,32 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent data-overlayscrollbars-initialize>
<OverlayScrollbar>
<ChildrenContainer>
<ChildrenContainer
style={{
margin:
isHome() ||
isLogsView() ||
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView() ||
isAlertHistory() ||
isAlertOverview() ||
isMessagingQueues() ||
isCloudIntegrationPage() ||
isInfraMonitoring() ||
isApiMonitoringView() ||
isExceptionsView()
? 0
: '0 1rem',
...(isTraceDetailsView() ||
isTracesFunnels() ||
isTracesFunnelDetails()
? { margin: 0 }
: {}),
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />}
{children}
</ChildrenContainer>

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
import './CreateAlertChannels.styles.scss';
import { Form } from 'antd';
import createEmail from 'api/channels/createEmail';
import createMsTeamsApi from 'api/channels/createMsTeams';
@@ -479,28 +477,26 @@ function CreateAlertChannels({
);
return (
<div className="create-alert-channels-container">
<FormAlertChannels
{...{
formInstance,
onTypeChangeHandler,
setSelectedConfig,
<FormAlertChannels
{...{
formInstance,
onTypeChangeHandler,
setSelectedConfig,
type,
onTestHandler,
onSaveHandler,
savingState,
testingState,
title: t('page_title_create'),
initialValue: {
type,
onTestHandler,
onSaveHandler,
savingState,
testingState,
title: t('page_title_create'),
initialValue: {
type,
...selectedConfig,
...PagerInitialConfig,
...OpsgenieInitialConfig,
...EmailInitialConfig,
},
}}
/>
</div>
...selectedConfig,
...PagerInitialConfig,
...OpsgenieInitialConfig,
...EmailInitialConfig,
},
}}
/>
);
}

View File

@@ -54,7 +54,6 @@ import {
X,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { FormattingOptions } from 'providers/preferences/types';
import {
CSSProperties,
Dispatch,
@@ -271,26 +270,17 @@ function ExplorerOptions({
const getUpdatedExtraData = (
extraData: string | undefined,
newSelectedColumns: BaseAutocompleteData[],
formattingOptions?: FormattingOptions,
): string => {
let updatedExtraData;
if (extraData) {
const parsedExtraData = JSON.parse(extraData);
parsedExtraData.selectColumns = newSelectedColumns;
if (formattingOptions) {
parsedExtraData.format = formattingOptions.format;
parsedExtraData.maxLines = formattingOptions.maxLines;
parsedExtraData.fontSize = formattingOptions.fontSize;
}
updatedExtraData = JSON.stringify(parsedExtraData);
} else {
updatedExtraData = JSON.stringify({
color: Color.BG_SIENNA_500,
selectColumns: newSelectedColumns,
format: formattingOptions?.format,
maxLines: formattingOptions?.maxLines,
fontSize: formattingOptions?.fontSize,
});
}
return updatedExtraData;
@@ -299,14 +289,6 @@ function ExplorerOptions({
const updatedExtraData = getUpdatedExtraData(
extraData,
options?.selectColumns,
// pass this only for logs
sourcepage === DataSource.LOGS
? {
format: options?.format,
maxLines: options?.maxLines,
fontSize: options?.fontSize,
}
: undefined,
);
const {
@@ -535,14 +517,6 @@ function ExplorerOptions({
color,
selectColumns: options.selectColumns,
version: 1,
...// pass this only for logs
(sourcepage === DataSource.LOGS
? {
format: options?.format,
maxLines: options?.maxLines,
fontSize: options?.fontSize,
}
: {}),
}),
notifications,
panelType: panelType || PANEL_TYPES.LIST,

View File

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

View File

@@ -85,13 +85,7 @@ function LabelSelect({
}, [handleBlur]);
const handleLabelChange = (event: ChangeEvent<HTMLInputElement>): void => {
// Remove the colon if it's the last character.
// As the colon is used to separate the key and value in the query.
setCurrentVal(
event.target?.value.endsWith(':')
? event.target?.value.slice(0, -1)
: event.target?.value,
);
setCurrentVal(event.target?.value.replace(':', ''));
};
const handleClose = (key: string): void => {

View File

@@ -12,7 +12,6 @@ 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';
@@ -185,25 +184,18 @@ export default function Home(): JSX.Element {
);
const processUserPreferences = (userPreferences: UserPreference[]): void => {
const checklistSkipped = Boolean(
userPreferences?.find(
(preference) =>
preference.name === ORG_PREFERENCES.WELCOME_CHECKLIST_DO_LATER,
)?.value,
);
const checklistSkipped = userPreferences?.find(
(preference) => preference.name === 'welcome_checklist_do_later',
)?.value;
const updatedChecklistItems = cloneDeep(checklistItems);
const newChecklistItems = updatedChecklistItems.map((item) => {
const newItem = { ...item };
const isSkipped = Boolean(
newItem.isSkipped =
userPreferences?.find(
(preference) => preference.name === item.skippedPreferenceKey,
)?.value,
);
newItem.isSkipped = isSkipped || false;
)?.value || false;
return newItem;
});
@@ -247,7 +239,7 @@ export default function Home(): JSX.Element {
setUpdatingUserPreferences(true);
updateUserPreference({
name: ORG_PREFERENCES.WELCOME_CHECKLIST_DO_LATER,
name: 'welcome_checklist_do_later',
value: true,
});
};

View File

@@ -1,19 +1,17 @@
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { ChecklistItem } from './HomeChecklist/HomeChecklist';
export const checkListStepToPreferenceKeyMap = {
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,
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',
};
export const DOCS_LINKS = {

View File

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

View File

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

View File

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

View File

@@ -114,6 +114,7 @@ function LogsExplorerViews({
// Context
const {
initialDataSource,
currentQuery,
stagedQuery,
panelType,
@@ -143,7 +144,7 @@ function LogsExplorerViews({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: DataSource.LOGS,
dataSource: initialDataSource || DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -5,7 +5,6 @@ import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_qu
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { VirtuosoMockContext } from 'react-virtuoso';
import { fireEvent, render, RenderResult } from 'tests/test-utils';
@@ -88,25 +87,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
// Mock usePreferenceSync
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
formatting: {
maxLines: 2,
format: 'table',
fontSize: 'small',
version: 1,
},
},
loading: false,
error: null,
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
}),
}));
jest.mock('hooks/logs/useCopyLogLink', () => ({
useCopyLogLink: jest.fn().mockReturnValue({
activeLogId: ACTIVE_LOG_ID,
@@ -125,15 +105,13 @@ const renderer = (): RenderResult =>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<PreferenceContextProvider>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</PreferenceContextProvider>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</VirtuosoMockContext.Provider>,
);
@@ -206,15 +184,13 @@ describe('LogsExplorerViews -', () => {
lodsQueryServerRequest();
render(
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
<PreferenceContextProvider>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</PreferenceContextProvider>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</QueryBuilderContext.Provider>,
);

View File

@@ -5,7 +5,6 @@ import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { I18nextProvider } from 'react-i18next';
import i18n from 'ReactI18';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
@@ -109,13 +108,11 @@ describe('LogsPanelComponent', () => {
render(
<I18nextProvider i18n={i18n}>
<DashboardProvider>
<PreferenceContextProvider>
<NewWidget
selectedGraph={PANEL_TYPES.LIST}
fillSpans={undefined}
yAxisUnit={undefined}
/>
</PreferenceContextProvider>
<NewWidget
selectedGraph={PANEL_TYPES.LIST}
fillSpans={undefined}
yAxisUnit={undefined}
/>
</DashboardProvider>
</I18nextProvider>,
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,115 +1,35 @@
import '../MySettings.styles.scss';
import './UserInfo.styles.scss';
import { Button, Input, Modal, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import changeMyPassword from 'api/v1/factor_password/changeMyPassword';
import { Button, Card, Flex, Input, Space, Typography } from 'antd';
import editUser from 'api/v1/user/id/update';
import { useNotifications } from 'hooks/useNotifications';
import { Check, FileTerminal, MailIcon, UserIcon } from 'lucide-react';
import { isPasswordValid } from 'pages/SignUp/utils';
import { PencilIcon } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { 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(['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 { t } = useTranslation();
const [changedName, setChangedName] = useState<string>(
user?.displayName || '',
);
const [loading, setLoading] = useState<boolean>(false);
const [isUpdateNameModalOpen, setIsUpdateNameModalOpen] = useState<boolean>(
false,
);
const [
isResetPasswordModalOpen,
setIsResetPasswordModalOpen,
] = useState<boolean>(false);
const { notifications } = useNotifications();
const defaultPlaceHolder = '*************';
useEffect(() => {
if (currentPassword && !isPasswordValid(currentPassword)) {
setIsPasswordPolicyError(true);
} else {
setIsPasswordPolicyError(false);
}
}, [currentPassword]);
if (!user) {
if (!user || !org) {
return <div />;
}
const hideUpdateNameModal = (): void => {
setIsUpdateNameModalOpen(false);
};
const hideResetPasswordModal = (): void => {
setIsResetPasswordModalOpen(false);
};
const onChangePasswordClickHandler = async (): Promise<void> => {
const onClickUpdateHandler = async (): Promise<void> => {
try {
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);
setLoading(true);
await editUser({
displayName: changedName,
userId: user.id,
@@ -124,143 +44,80 @@ function UserInfo(): JSX.Element {
...user,
displayName: changedName,
});
setIsLoading(false);
hideUpdateNameModal();
setLoading(false);
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
}
setIsLoading(false);
setLoading(false);
};
if (!user || !org) {
return <div />;
}
return (
<div className="user-info-card">
<div className="user-info">
<div className="user-name">{user.displayName}</div>
<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-subsection">
<div className="user-email">
<MailIcon size={16} /> {user.email}
</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-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
key="submit"
className="flexBtn"
loading={loading}
disabled={loading}
onClick={onClickUpdateHandler}
data-testid="update-name-button"
type="primary"
icon={<Check size={16} />}
onClick={onSaveHandler}
disabled={isLoading}
data-testid="update-name-btn"
>
Update name
</Button>,
]}
>
<Typography.Text>Name</Typography.Text>
<div className="update-name-input">
<PencilIcon size={12} /> Update
</Button>
</Flex>
<Space>
<Typography className="userInfo-label" data-testid="email-label">
{' '}
Email{' '}
</Typography>
<Input
placeholder="e.g. John Doe"
value={changedName}
onChange={(e): void => setChangedName(e.target.value)}
className="userInfo-value"
data-testid="email-textbox"
value={user.email}
disabled
/>
</div>
</Modal>
</Space>
<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>
<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>
);
}

View File

@@ -2,21 +2,17 @@ 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(() => true),
useIsDarkMode: jest.fn(() => ({
toggleTheme: toggleThemeFunction,
})),
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', () => ({
@@ -29,97 +25,90 @@ jest.mock('hooks/useNotifications', () => ({
})),
}));
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';
enum ThemeOptions {
Dark = 'Dark',
Light = 'Light Beta',
}
describe('MySettings Flows', () => {
beforeEach(() => {
jest.clearAllMocks();
render(<MySettingsContainer />);
});
describe('Dark/Light Theme Switch', () => {
it('Should display Dark and Light theme options properly', async () => {
// Check Dark theme option
it('Should display Dark and Light theme buttons properly', async () => {
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 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 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 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);
it('Should switch the them on clicking Light theme', async () => {
const lightThemeOption = screen.getByRole('radio', {
name: /light/i,
});
fireEvent.click(lightThemeOption);
await waitFor(() => {
expect(toggleThemeFunction).toHaveBeenCalled();
expect(logEventFunction).toHaveBeenCalledWith(
'Account Settings: Theme Changed',
{
theme: 'light',
},
);
expect(toggleThemeFunction).toBeCalled();
});
});
});
describe('User Details Form', () => {
it('Should properly display the User Details Form', () => {
// 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);
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');
expect(userDetailsHeader).toBeInTheDocument();
expect(nameLabel).toBeInTheDocument();
expect(nameTextbox).toBeInTheDocument();
expect(modalUpdateNameButton).toBeInTheDocument();
expect(updateNameButton).toBeInTheDocument();
expect(emailLabel).toBeInTheDocument();
expect(emailTextbox).toBeInTheDocument();
expect(roleLabel).toBeInTheDocument();
expect(roleTextbox).toBeInTheDocument();
});
it('Should update the name on clicking Update button', async () => {
// 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);
const nameTextbox = screen.getByTestId('name-textbox');
const updateNameButton = screen.getByTestId('update-name-button');
act(() => {
fireEvent.change(nameTextbox, { target: { value: 'New Name' } });
});
fireEvent.click(modalUpdateNameButton);
fireEvent.click(updateNameButton);
await waitFor(() =>
expect(successNotification).toHaveBeenCalledWith({
@@ -130,53 +119,92 @@ describe('MySettings Flows', () => {
});
describe('Reset password', () => {
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]);
let currentPasswordTextbox: Node | Window;
let newPasswordTextbox: Node | Window;
let submitButtonElement: HTMLElement;
// 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();
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');
});
it('Should display validation error if password is less than 8 characters', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
const currentPasswordTextbox = screen.getByTestId(
'current-password-textbox',
);
act(() => {
fireEvent.change(currentPasswordTextbox, { target: { value: '123' } });
});
const validationMessage = await screen.findByTestId('validation-message');
await waitFor(() => {
// 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();
}
expect(validationMessage).toHaveTextContent(
'Password must a have minimum of 8 characters',
);
});
});
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]);
test("Should display 'inavlid credentials' error if different current and new passwords are provided", async () => {
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123456879' },
});
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
const newPasswordTextbox = screen.getByTestId(NEW_PASSWORD_TEST_ID);
const submitButton = screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID);
fireEvent.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();
act(() => {
fireEvent.change(currentPasswordTextbox, {
@@ -185,25 +213,7 @@ describe('MySettings Flows', () => {
fireEvent.change(newPasswordTextbox, { target: { value: '123456789' } });
});
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();
expect(submitButtonElement).toBeDisabled();
});
});
});

View File

@@ -1,52 +1,18 @@
import './MySettings.styles.scss';
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 { Button, Radio, RadioChangeEvent, Space, Tag, Typography } from 'antd';
import { Logout } from 'api/utils';
import useThemeMode, { useIsDarkMode } from 'hooks/useDarkMode';
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 { LogOut, Moon, Sun } from 'lucide-react';
import { useState } from 'react';
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 = [
{
@@ -73,112 +39,57 @@ 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 (
<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>
<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>
<div className="user-preference-section">
<div className="user-preference-section-header">
<div className="user-preference-section-title">User Preferences</div>
<div className="user-preference-section-subtitle">
Tailor the SigNoz console to work according to your needs.
</div>
</div>
<div className="user-preference-section-content">
<div className="user-preference-section-content-item theme-selector">
<div className="user-preference-section-content-item-title-action">
Select your theme
<Radio.Group
options={themeOptions}
onChange={handleThemeChange}
value={theme}
optionType="button"
buttonStyle="solid"
data-testid="theme-selector"
size="small"
/>
</div>
<div className="user-preference-section-content-item-description">
Select if SigNoz&apos;s appearance should be light or dark
</div>
</div>
<TimezoneAdaptation />
<div className="user-preference-section-content-item">
<div className="user-preference-section-content-item-title-action">
Keep the primary sidebar always open{' '}
<Switch
checked={sideNavPinned}
onChange={handleSideNavPinnedChange}
loading={isUpdatingUserPreference}
/>
</div>
<div className="user-preference-section-content-item-description">
Keep the primary sidebar always open by default, unless collapsed with
the keyboard shortcut
</div>
</div>
</div>
<div className="user-info-container">
<UserInfo />
</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>
);
}

View File

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

View File

@@ -5,8 +5,8 @@ export const RIBBON_STYLES = {
};
export const buttonText: Record<string, string> = {
[ROUTES.LOGS_EXPLORER]: 'Old Explorer',
[ROUTES.TRACE]: 'New Explorer',
[ROUTES.OLD_LOGS_EXPLORER]: 'New Explorer',
[ROUTES.TRACES_EXPLORER]: 'Old Explorer',
[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',
};

View File

@@ -1,9 +1,7 @@
import './NewExplorerCTA.styles.scss';
import { CompassOutlined } from '@ant-design/icons';
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';
@@ -36,11 +34,11 @@ function NewExplorerCTA(): JSX.Element | null {
const button = useMemo(
() => (
<Button
icon={<Undo size={16} />}
icon={<CompassOutlined />}
onClick={onClickHandler}
danger
data-testid="newExplorerCTA"
type="text"
className="periscope-btn link"
type="primary"
>
{buttonText[location.pathname]}
</Button>

View File

@@ -8,7 +8,6 @@ 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';
@@ -197,7 +196,7 @@ function OnboardingQuestionaire(): JSX.Element {
setUpdatingOrgOnboardingStatus(true);
updateOrgPreference({
name: ORG_PREFERENCES.ORG_ONBOARDING,
name: 'org_onboarding',
value: true,
});
};

View File

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

View File

@@ -1,4 +1,7 @@
import getFromLocalstorage from 'api/browser/localstorage/get';
import setToLocalstorage from 'api/browser/localstorage/set';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { LOCALSTORAGE } from 'constants/localStorage';
import { LogViewMode } from 'container/LogsTable';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import useDebounce from 'hooks/useDebounce';
@@ -8,7 +11,6 @@ import {
AllTraceFilterKeys,
AllTraceFilterKeyValue,
} from 'pages/TracesExplorer/Filter/filterUtils';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueries } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -33,10 +35,10 @@ import {
import { getOptionsFromKeys } from './utils';
interface UseOptionsMenuProps {
storageKey?: string;
dataSource: DataSource;
aggregateOperator: string;
initialOptions?: InitialOptions;
storageKey: LOCALSTORAGE;
}
interface UseOptionsMenu {
@@ -46,21 +48,22 @@ interface UseOptionsMenu {
}
const useOptionsMenu = ({
storageKey,
dataSource,
aggregateOperator,
initialOptions = {},
}: UseOptionsMenuProps): UseOptionsMenu => {
const { notifications } = useNotifications();
const {
preferences,
updateColumns,
updateFormatting,
} = usePreferenceContext();
const [searchText, setSearchText] = useState<string>('');
const [isFocused, setIsFocused] = useState<boolean>(false);
const debouncedSearchText = useDebounce(searchText, 300);
const localStorageOptionsQuery = useMemo(
() => getFromLocalstorage(storageKey),
[storageKey],
);
const initialQueryParams = useMemo(
() => ({
searchText: '',
@@ -74,6 +77,7 @@ const useOptionsMenu = ({
const {
query: optionsQuery,
queryData: optionsQueryData,
redirectWithQuery: redirectWithOptionsData,
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
@@ -101,9 +105,7 @@ const useOptionsMenu = ({
);
const initialSelectedColumns = useMemo(() => {
if (!isFetchedInitialAttributes) {
return [];
}
if (!isFetchedInitialAttributes) return [];
const attributesData = initialAttributesResult?.reduce(
(acc, attributeResponse) => {
@@ -140,12 +142,14 @@ const useOptionsMenu = ({
})
.filter(Boolean) as BaseAutocompleteData[];
// this is the last point where we can set the default columns and if uptil now also we have an empty array then we will set the default columns
if (!initialSelected || !initialSelected?.length) {
initialSelected = defaultTraceSelectedColumns;
}
}
return initialSelected || [];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isFetchedInitialAttributes,
initialOptions?.selectColumns,
@@ -167,6 +171,7 @@ const useOptionsMenu = ({
const searchedAttributeKeys = useMemo(() => {
if (searchedAttributesData?.payload?.attributeKeys?.length) {
if (dataSource === DataSource.LOGS) {
// add timestamp and body to the list of attributes
return [
...defaultLogsSelectedColumns,
...searchedAttributesData.payload.attributeKeys.filter(
@@ -183,35 +188,32 @@ const useOptionsMenu = ({
return [];
}, [dataSource, searchedAttributesData?.payload?.attributeKeys]);
const initialOptionsQuery: OptionsQuery = useMemo(() => {
let defaultColumns = defaultOptionsQuery.selectColumns;
if (dataSource === DataSource.TRACES) {
defaultColumns = defaultTraceSelectedColumns;
} else if (dataSource === DataSource.LOGS) {
defaultColumns = defaultLogsSelectedColumns;
}
const finalSelectColumns = initialOptions?.selectColumns
? initialSelectedColumns
: defaultColumns;
return {
const initialOptionsQuery: OptionsQuery = useMemo(
() => ({
...defaultOptionsQuery,
...initialOptions,
selectColumns: finalSelectColumns,
};
}, [dataSource, initialOptions, initialSelectedColumns]);
// eslint-disable-next-line no-nested-ternary
selectColumns: initialOptions?.selectColumns
? initialSelectedColumns
: dataSource === DataSource.TRACES
? defaultTraceSelectedColumns
: defaultOptionsQuery.selectColumns,
}),
[dataSource, initialOptions, initialSelectedColumns],
);
const selectedColumnKeys = useMemo(
() => preferences?.columns?.map(({ id }) => id) || [],
[preferences?.columns],
() => optionsQueryData?.selectColumns?.map(({ id }) => id) || [],
[optionsQueryData],
);
const optionsFromAttributeKeys = useMemo(() => {
const filteredAttributeKeys = searchedAttributeKeys.filter((item) => {
// For other data sources, only filter out 'body' if it exists
if (dataSource !== DataSource.LOGS) {
return item.key !== 'body';
}
// For LOGS, keep all keys
return true;
});
@@ -221,8 +223,10 @@ const useOptionsMenu = ({
const handleRedirectWithOptionsData = useCallback(
(newQueryData: OptionsQuery) => {
redirectWithOptionsData(newQueryData);
setToLocalstorage(storageKey, JSON.stringify(newQueryData));
},
[redirectWithOptionsData],
[storageKey, redirectWithOptionsData],
);
const handleSelectColumns = useCallback(
@@ -231,7 +235,7 @@ const useOptionsMenu = ({
const newSelectedColumns = newSelectedColumnKeys.reduce((acc, key) => {
const column = [
...searchedAttributeKeys,
...(preferences?.columns || []),
...optionsQueryData.selectColumns,
].find(({ id }) => id === key);
if (!column) return acc;
@@ -239,116 +243,75 @@ const useOptionsMenu = ({
}, [] as BaseAutocompleteData[]);
const optionsData: OptionsQuery = {
...defaultOptionsQuery,
...optionsQueryData,
selectColumns: newSelectedColumns,
format: preferences?.formatting?.format || defaultOptionsQuery.format,
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
};
updateColumns(newSelectedColumns);
handleRedirectWithOptionsData(optionsData);
},
[
searchedAttributeKeys,
selectedColumnKeys,
preferences,
optionsQueryData,
handleRedirectWithOptionsData,
updateColumns,
],
);
const handleRemoveSelectedColumn = useCallback(
(columnKey: string) => {
const newSelectedColumns = preferences?.columns?.filter(
const newSelectedColumns = optionsQueryData?.selectColumns?.filter(
({ id }) => id !== columnKey,
);
if (!newSelectedColumns?.length && dataSource !== DataSource.LOGS) {
if (!newSelectedColumns.length && dataSource !== DataSource.LOGS) {
notifications.error({
message: 'There must be at least one selected column',
});
} else {
const optionsData: OptionsQuery = {
...defaultOptionsQuery,
selectColumns: newSelectedColumns || [],
format: preferences?.formatting?.format || defaultOptionsQuery.format,
maxLines:
preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
fontSize:
preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
...optionsQueryData,
selectColumns: newSelectedColumns,
};
updateColumns(newSelectedColumns || []);
handleRedirectWithOptionsData(optionsData);
}
},
[
dataSource,
notifications,
preferences,
handleRedirectWithOptionsData,
updateColumns,
],
[dataSource, notifications, optionsQueryData, handleRedirectWithOptionsData],
);
const handleFormatChange = useCallback(
(value: LogViewMode) => {
const optionsData: OptionsQuery = {
...defaultOptionsQuery,
selectColumns: preferences?.columns || [],
...optionsQueryData,
format: value,
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
};
updateFormatting({
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
format: value,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
});
handleRedirectWithOptionsData(optionsData);
},
[handleRedirectWithOptionsData, preferences, updateFormatting],
[handleRedirectWithOptionsData, optionsQueryData],
);
const handleMaxLinesChange = useCallback(
(value: string | number | null) => {
const optionsData: OptionsQuery = {
...defaultOptionsQuery,
selectColumns: preferences?.columns || [],
format: preferences?.formatting?.format || defaultOptionsQuery.format,
...optionsQueryData,
maxLines: value as number,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
};
updateFormatting({
maxLines: value as number,
format: preferences?.formatting?.format || defaultOptionsQuery.format,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
});
handleRedirectWithOptionsData(optionsData);
},
[handleRedirectWithOptionsData, preferences, updateFormatting],
[handleRedirectWithOptionsData, optionsQueryData],
);
const handleFontSizeChange = useCallback(
(value: FontSize) => {
const optionsData: OptionsQuery = {
...defaultOptionsQuery,
selectColumns: preferences?.columns || [],
format: preferences?.formatting?.format || defaultOptionsQuery.format,
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
...optionsQueryData,
fontSize: value,
};
updateFormatting({
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
format: preferences?.formatting?.format || defaultOptionsQuery.format,
fontSize: value,
});
handleRedirectWithOptionsData(optionsData);
},
[handleRedirectWithOptionsData, preferences, updateFormatting],
[handleRedirectWithOptionsData, optionsQueryData],
);
const handleSearchAttribute = useCallback((value: string) => {
@@ -368,7 +331,7 @@ const useOptionsMenu = ({
() => ({
addColumn: {
isFetching: isSearchedAttributesFetching,
value: preferences?.columns || defaultOptionsQuery.selectColumns,
value: optionsQueryData?.selectColumns || defaultOptionsQuery.selectColumns,
options: optionsFromAttributeKeys || [],
onFocus: handleFocus,
onBlur: handleBlur,
@@ -377,21 +340,24 @@ const useOptionsMenu = ({
onSearch: handleSearchAttribute,
},
format: {
value: preferences?.formatting?.format || defaultOptionsQuery.format,
value: optionsQueryData.format || defaultOptionsQuery.format,
onChange: handleFormatChange,
},
maxLines: {
value: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
value: optionsQueryData.maxLines || defaultOptionsQuery.maxLines,
onChange: handleMaxLinesChange,
},
fontSize: {
value: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
value: optionsQueryData?.fontSize || defaultOptionsQuery.fontSize,
onChange: handleFontSizeChange,
},
}),
[
isSearchedAttributesFetching,
preferences,
optionsQueryData?.selectColumns,
optionsQueryData.format,
optionsQueryData.maxLines,
optionsQueryData?.fontSize,
optionsFromAttributeKeys,
handleSelectColumns,
handleRemoveSelectedColumn,
@@ -403,25 +369,23 @@ const useOptionsMenu = ({
);
useEffect(() => {
if (optionsQuery || !isFetchedInitialAttributes) {
return;
}
if (optionsQuery || !isFetchedInitialAttributes) return;
redirectWithOptionsData(initialOptionsQuery);
const nextOptionsQuery = localStorageOptionsQuery
? JSON.parse(localStorageOptionsQuery)
: initialOptionsQuery;
redirectWithOptionsData(nextOptionsQuery);
}, [
isFetchedInitialAttributes,
optionsQuery,
initialOptionsQuery,
localStorageOptionsQuery,
redirectWithOptionsData,
]);
return {
options: {
selectColumns: preferences?.columns || [],
format: preferences?.formatting?.format || defaultOptionsQuery.format,
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
},
options: optionsQueryData,
config: optionsMenuConfig,
handleOptionsChange: handleRedirectWithOptionsData,
};

View File

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

View File

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

View File

@@ -18,27 +18,6 @@
background-color: yellow;
border-radius: 6px;
cursor: pointer;
.event-dot {
position: absolute;
top: 50%;
transform: translate(-50%, -50%) rotate(45deg);
width: 6px;
height: 6px;
background-color: var(--bg-robin-500);
border: 1px solid var(--bg-robin-600);
cursor: pointer;
z-index: 1;
&.error {
background-color: var(--bg-cherry-500);
border-color: var(--bg-cherry-600);
}
&:hover {
transform: translate(-50%, -50%) rotate(45deg) scale(1.5);
}
}
}
}

View File

@@ -6,7 +6,6 @@ import { Tooltip } from 'antd';
import Color from 'color';
import TimelineV2 from 'components/TimelineV2/TimelineV2';
import { themeColors } from 'constants/theme';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import {
@@ -21,7 +20,6 @@ import { useHistory, useLocation } from 'react-router-dom';
import { ListRange, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
import { Span } from 'types/api/trace/getTraceV2';
import { toFixed } from 'utils/toFixed';
interface ITraceMetadata {
startTime: number;
@@ -93,31 +91,7 @@ function Success(props: ISuccessProps): JSX.Element {
searchParams.set('spanId', span.spanId);
history.replace({ search: searchParams.toString() });
}}
>
{span.event?.map((event) => {
const eventTimeMs = event.timeUnixNano / 1e6;
const eventOffsetPercent =
((eventTimeMs - span.timestamp) / (span.durationNano / 1e6)) * 100;
const clampedOffset = Math.max(1, Math.min(eventOffsetPercent, 99));
const { isError } = event;
const { time, timeUnitName } = convertTimeToRelevantUnit(
eventTimeMs - span.timestamp,
);
return (
<Tooltip
key={`${span.spanId}-event-${event.name}-${event.timeUnixNano}`}
title={`${event.name} @ ${toFixed(time, 2)} ${timeUnitName}`}
>
<div
className={`event-dot ${isError ? 'error' : ''}`}
style={{
left: `${clampedOffset}%`,
}}
/>
</Tooltip>
);
})}
</div>
/>
</Tooltip>
);
})}

View File

@@ -1,6 +1,5 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { screen } from '@testing-library/react';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { findByText, fireEvent, render, waitFor } from 'tests/test-utils';
import { pipelineApiResponseMockData } from '../mocks/pipeline';
@@ -20,18 +19,6 @@ jest.mock('uplot', () => {
};
});
// Mock useUrlQuery hook
const mockUrlQuery = {
get: jest.fn(),
set: jest.fn(),
toString: jest.fn(() => ''),
};
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: jest.fn(() => mockUrlQuery),
}));
const samplePipelinePreviewResponse = {
isLoading: false,
logs: [
@@ -70,38 +57,17 @@ jest.mock(
}),
);
// Mock usePreferenceSync
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
formatting: {
maxLines: 2,
format: 'table',
fontSize: 'small',
version: 1,
},
},
loading: false,
error: null,
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
}),
}));
describe('PipelinePage container test', () => {
it('should render PipelineListsView section', () => {
const { getByText, container } = render(
<PreferenceContextProvider>
<PipelineListsView
setActionType={jest.fn()}
isActionMode="viewing-mode"
setActionMode={jest.fn()}
pipelineData={pipelineApiResponseMockData}
isActionType=""
refetchPipelineLists={jest.fn()}
/>
</PreferenceContextProvider>,
<PipelineListsView
setActionType={jest.fn()}
isActionMode="viewing-mode"
setActionMode={jest.fn()}
pipelineData={pipelineApiResponseMockData}
isActionType=""
refetchPipelineLists={jest.fn()}
/>,
);
// table headers assertions
@@ -125,16 +91,14 @@ describe('PipelinePage container test', () => {
it('should render expanded content and edit mode correctly', async () => {
const { getByText } = render(
<PreferenceContextProvider>
<PipelineListsView
setActionType={jest.fn()}
isActionMode="editing-mode"
setActionMode={jest.fn()}
pipelineData={pipelineApiResponseMockData}
isActionType=""
refetchPipelineLists={jest.fn()}
/>
</PreferenceContextProvider>,
<PipelineListsView
setActionType={jest.fn()}
isActionMode="editing-mode"
setActionMode={jest.fn()}
pipelineData={pipelineApiResponseMockData}
isActionType=""
refetchPipelineLists={jest.fn()}
/>,
);
// content assertion
@@ -158,16 +122,14 @@ describe('PipelinePage container test', () => {
it('should be able to perform actions and edit on expanded view content', async () => {
render(
<PreferenceContextProvider>
<PipelineListsView
setActionType={jest.fn()}
isActionMode="editing-mode"
setActionMode={jest.fn()}
pipelineData={pipelineApiResponseMockData}
isActionType=""
refetchPipelineLists={jest.fn()}
/>
</PreferenceContextProvider>,
<PipelineListsView
setActionType={jest.fn()}
isActionMode="editing-mode"
setActionMode={jest.fn()}
pipelineData={pipelineApiResponseMockData}
isActionType=""
refetchPipelineLists={jest.fn()}
/>,
);
// content assertion
@@ -218,16 +180,14 @@ describe('PipelinePage container test', () => {
it('should be able to toggle and delete pipeline', async () => {
const { getByText } = render(
<PreferenceContextProvider>
<PipelineListsView
setActionType={jest.fn()}
isActionMode="editing-mode"
setActionMode={jest.fn()}
pipelineData={pipelineApiResponseMockData}
isActionType=""
refetchPipelineLists={jest.fn()}
/>
</PreferenceContextProvider>,
<PipelineListsView
setActionType={jest.fn()}
isActionMode="editing-mode"
setActionMode={jest.fn()}
pipelineData={pipelineApiResponseMockData}
isActionType=""
refetchPipelineLists={jest.fn()}
/>,
);
const addNewPipelineBtn = getByText('add_new_pipeline');
@@ -287,16 +247,14 @@ describe('PipelinePage container test', () => {
it('should have populated form fields when edit pipeline is clicked', async () => {
render(
<PreferenceContextProvider>
<PipelineListsView
setActionType={jest.fn()}
isActionMode="editing-mode"
setActionMode={jest.fn()}
pipelineData={pipelineApiResponseMockData}
isActionType="edit-pipeline"
refetchPipelineLists={jest.fn()}
/>
</PreferenceContextProvider>,
<PipelineListsView
setActionType={jest.fn()}
isActionMode="editing-mode"
setActionMode={jest.fn()}
pipelineData={pipelineApiResponseMockData}
isActionType="edit-pipeline"
refetchPipelineLists={jest.fn()}
/>,
);
// content assertion

View File

@@ -324,7 +324,7 @@ export const Query = memo(function Query({
]);
const disableOperatorSelector =
!query?.aggregateAttribute?.key || query?.aggregateAttribute?.key === '';
!query?.aggregateAttribute.key || query?.aggregateAttribute.key === '';
const isVersionV4 = version && version === ENTITY_VERSION_V4;

View File

@@ -1037,9 +1037,7 @@ function QueryBuilderSearchV2(
);
})}
</Select>
{!hideSpanScopeSelector && (
<SpanScopeSelector query={query} onChange={onChange} />
)}
{!hideSpanScopeSelector && <SpanScopeSelector queryName={query.queryName} />}
</div>
);
}

View File

@@ -2,11 +2,7 @@ import { Select } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep } from 'lodash-es';
import { useEffect, useState } from 'react';
import {
IBuilderQuery,
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
enum SpanScope {
@@ -21,8 +17,7 @@ interface SpanFilterConfig {
}
interface SpanScopeSelectorProps {
onChange?: (value: TagFilter) => void;
query?: IBuilderQuery;
queryName: string;
}
const SPAN_FILTER_CONFIG: Record<SpanScope, SpanFilterConfig | null> = {
@@ -55,10 +50,7 @@ const SELECT_OPTIONS = [
{ value: SpanScope.ENTRYPOINT_SPANS, label: 'Entrypoint Spans' },
];
function SpanScopeSelector({
onChange,
query,
}: SpanScopeSelectorProps): JSX.Element {
function SpanScopeSelector({ queryName }: SpanScopeSelectorProps): JSX.Element {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const [selectedScope, setSelectedScope] = useState<SpanScope>(
SpanScope.ALL_SPANS,
@@ -68,7 +60,7 @@ function SpanScopeSelector({
filters: TagFilterItem[] = [],
): SpanScope => {
const hasFilter = (key: string): boolean =>
filters?.some(
filters.some(
(filter) =>
filter.key?.type === 'spanSearchScope' &&
filter.key.key === key &&
@@ -79,19 +71,15 @@ function SpanScopeSelector({
if (hasFilter('isEntryPoint')) return SpanScope.ENTRYPOINT_SPANS;
return SpanScope.ALL_SPANS;
};
useEffect(() => {
let queryData = (currentQuery?.builder?.queryData || [])?.find(
(item) => item.queryName === query?.queryName,
const queryData = (currentQuery?.builder?.queryData || [])?.find(
(item) => item.queryName === queryName,
);
if (onChange && query) {
queryData = query;
}
const filters = queryData?.filters?.items;
const currentScope = getCurrentScopeFromFilters(filters);
setSelectedScope(currentScope);
}, [currentQuery, onChange, query]);
}, [currentQuery, queryName]);
const handleScopeChange = (newScope: SpanScope): void => {
const newQuery = cloneDeep(currentQuery);
@@ -120,28 +108,14 @@ function SpanScopeSelector({
...item,
filters: {
...item.filters,
items: getUpdatedFilters(
item.filters?.items,
item.queryName === query?.queryName,
),
items: getUpdatedFilters(item.filters?.items, item.queryName === queryName),
},
}));
if (onChange && query) {
onChange({
...query.filters,
items: getUpdatedFilters(
[...query.filters.items, ...newQuery.builder.queryData[0].filters.items],
true,
),
});
setSelectedScope(newScope);
} else {
redirectWithQueryBuilderData(newQuery);
}
redirectWithQueryBuilderData(newQuery);
};
//
return (
<Select
value={selectedScope}
@@ -153,9 +127,4 @@ function SpanScopeSelector({
);
}
SpanScopeSelector.defaultProps = {
onChange: undefined,
query: undefined,
};
export default SpanScopeSelector;

View File

@@ -6,12 +6,7 @@ import {
} from '@testing-library/react';
import { initialQueriesMap } from 'constants/queryBuilder';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import {
IBuilderQuery,
Query,
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import SpanScopeSelector from '../SpanScopeSelector';
@@ -28,13 +23,6 @@ const createSpanScopeFilter = (key: string): TagFilterItem => ({
value: 'true',
});
const createNonScopeFilter = (key: string, value: string): TagFilterItem => ({
id: `non-scope-${key}`,
key: { key, isColumn: false, type: 'tag' },
op: '=',
value,
});
const defaultQuery = {
...initialQueriesMap.traces,
builder: {
@@ -48,12 +36,6 @@ const defaultQuery = {
},
};
const defaultQueryBuilderQuery: IBuilderQuery = {
...initialQueriesMap.traces.builder.queryData[0],
queryName: 'A',
filters: { items: [], op: 'AND' },
};
// Helper to create query with filters
const createQueryWithFilters = (filters: TagFilterItem[]): Query => ({
...defaultQuery,
@@ -62,7 +44,6 @@ const createQueryWithFilters = (filters: TagFilterItem[]): Query => ({
queryData: [
{
...defaultQuery.builder.queryData[0],
queryName: 'A',
filters: {
items: filters,
op: 'AND',
@@ -73,9 +54,8 @@ const createQueryWithFilters = (filters: TagFilterItem[]): Query => ({
});
const renderWithContext = (
queryName = 'A',
initialQuery = defaultQuery,
onChangeProp?: (value: TagFilter) => void,
queryProp?: IBuilderQuery,
): RenderResult =>
render(
<QueryBuilderContext.Provider
@@ -87,24 +67,10 @@ const renderWithContext = (
} as any
}
>
<SpanScopeSelector onChange={onChangeProp} query={queryProp} />
<SpanScopeSelector queryName={queryName} />
</QueryBuilderContext.Provider>,
);
const selectOption = async (optionText: string): Promise<void> => {
const selector = screen.getByRole('combobox');
fireEvent.mouseDown(selector);
// Wait for dropdown to appear
await screen.findByRole('listbox');
// Find the option by its content text and click it
const option = await screen.findByText(optionText, {
selector: '.ant-select-item-option-content',
});
fireEvent.click(option);
};
describe('SpanScopeSelector', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -116,6 +82,13 @@ describe('SpanScopeSelector', () => {
});
describe('when selecting different options', () => {
const selectOption = (optionText: string): void => {
const selector = screen.getByRole('combobox');
fireEvent.mouseDown(selector);
const option = screen.getByText(optionText);
fireEvent.click(option);
};
const assertFilterAdded = (
updatedQuery: Query,
expectedKey: string,
@@ -133,13 +106,13 @@ describe('SpanScopeSelector', () => {
);
};
it('should remove span scope filters when selecting ALL_SPANS', async () => {
it('should remove span scope filters when selecting ALL_SPANS', () => {
const queryWithSpanScope = createQueryWithFilters([
createSpanScopeFilter('isRoot'),
]);
renderWithContext(queryWithSpanScope, undefined, defaultQueryBuilderQuery);
renderWithContext('A', queryWithSpanScope);
await selectOption('All Spans');
selectOption('All Spans');
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
const updatedQuery = mockRedirectWithQueryBuilderData.mock.calls[0][0];
@@ -152,8 +125,7 @@ describe('SpanScopeSelector', () => {
});
it('should add isRoot filter when selecting ROOT_SPANS', async () => {
renderWithContext(defaultQuery, undefined, defaultQueryBuilderQuery);
// eslint-disable-next-line sonarjs/no-duplicate-string
renderWithContext();
await selectOption('Root Spans');
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
@@ -163,10 +135,9 @@ describe('SpanScopeSelector', () => {
);
});
it('should add isEntryPoint filter when selecting ENTRYPOINT_SPANS', async () => {
renderWithContext(defaultQuery, undefined, defaultQueryBuilderQuery);
// eslint-disable-next-line sonarjs/no-duplicate-string
await selectOption('Entrypoint Spans');
it('should add isEntryPoint filter when selecting ENTRYPOINT_SPANS', () => {
renderWithContext();
selectOption('Entrypoint Spans');
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
assertFilterAdded(
@@ -186,180 +157,9 @@ describe('SpanScopeSelector', () => {
const queryWithFilter = createQueryWithFilters([
createSpanScopeFilter(filterKey),
]);
renderWithContext(queryWithFilter, undefined, defaultQueryBuilderQuery);
renderWithContext('A', queryWithFilter);
expect(await screen.findByText(expectedText)).toBeInTheDocument();
},
);
});
describe('when onChange and query props are provided', () => {
const mockOnChange = jest.fn();
const createLocalQuery = (
filterItems: TagFilterItem[] = [],
op: 'AND' | 'OR' = 'AND',
): IBuilderQuery => ({
...defaultQueryBuilderQuery,
filters: { items: filterItems, op },
});
const assertOnChangePayload = (
callNumber: number, // To handle multiple calls if needed, usually 0 for single interaction
expectedScopeKey: string | null,
expectedNonScopeItems: TagFilterItem[] = [],
): void => {
expect(mockOnChange).toHaveBeenCalled();
const onChangeArg = mockOnChange.mock.calls[callNumber][0] as TagFilter;
const { items } = onChangeArg;
// Check for preservation of specific non-scope items
expectedNonScopeItems.forEach((nonScopeItem) => {
expect(items).toContainEqual(nonScopeItem);
});
const scopeFiltersInPayload = items.filter(
(filter) => filter.key?.type === 'spanSearchScope',
);
if (expectedScopeKey) {
expect(scopeFiltersInPayload.length).toBe(1);
expect(scopeFiltersInPayload[0].key?.key).toBe(expectedScopeKey);
expect(scopeFiltersInPayload[0].value).toBe('true');
expect(scopeFiltersInPayload[0].op).toBe('=');
} else {
expect(scopeFiltersInPayload.length).toBe(0);
}
const expectedTotalFilters =
expectedNonScopeItems.length + (expectedScopeKey ? 1 : 0);
expect(items.length).toBe(expectedTotalFilters);
};
beforeEach(() => {
mockOnChange.mockClear();
mockRedirectWithQueryBuilderData.mockClear();
});
it('should initialize with ALL_SPANS if query prop has no scope filters', async () => {
const localQuery = createLocalQuery();
renderWithContext(defaultQuery, mockOnChange, localQuery);
expect(await screen.findByText('All Spans')).toBeInTheDocument();
});
it('should initialize with ROOT_SPANS if query prop has isRoot filter', async () => {
const localQuery = createLocalQuery([createSpanScopeFilter('isRoot')]);
renderWithContext(defaultQuery, mockOnChange, localQuery);
expect(await screen.findByText('Root Spans')).toBeInTheDocument();
});
it('should initialize with ENTRYPOINT_SPANS if query prop has isEntryPoint filter', async () => {
const localQuery = createLocalQuery([createSpanScopeFilter('isEntryPoint')]);
renderWithContext(defaultQuery, mockOnChange, localQuery);
expect(await screen.findByText('Entrypoint Spans')).toBeInTheDocument();
});
it('should call onChange and not redirect when selecting ROOT_SPANS (from ALL_SPANS)', async () => {
const localQuery = createLocalQuery(); // Initially All Spans
const { container } = renderWithContext(
defaultQuery,
mockOnChange,
localQuery,
);
expect(await screen.findByText('All Spans')).toBeInTheDocument();
await selectOption('Root Spans');
expect(mockRedirectWithQueryBuilderData).not.toHaveBeenCalled();
assertOnChangePayload(0, 'isRoot', []);
expect(
container.querySelector('span[title="Root Spans"]'),
).toBeInTheDocument();
});
it('should call onChange with removed scope when selecting ALL_SPANS (from ROOT_SPANS)', async () => {
const initialRootFilter = createSpanScopeFilter('isRoot');
const localQuery = createLocalQuery([initialRootFilter]);
const { container } = renderWithContext(
defaultQuery,
mockOnChange,
localQuery,
);
expect(await screen.findByText('Root Spans')).toBeInTheDocument();
await selectOption('All Spans');
expect(mockRedirectWithQueryBuilderData).not.toHaveBeenCalled();
assertOnChangePayload(0, null, []);
expect(
container.querySelector('span[title="All Spans"]'),
).toBeInTheDocument();
});
it('should call onChange, replacing isRoot with isEntryPoint', async () => {
const initialRootFilter = createSpanScopeFilter('isRoot');
const localQuery = createLocalQuery([initialRootFilter]);
const { container } = renderWithContext(
defaultQuery,
mockOnChange,
localQuery,
);
expect(await screen.findByText('Root Spans')).toBeInTheDocument();
await selectOption('Entrypoint Spans');
expect(mockRedirectWithQueryBuilderData).not.toHaveBeenCalled();
assertOnChangePayload(0, 'isEntryPoint', []);
expect(
container.querySelector('span[title="Entrypoint Spans"]'),
).toBeInTheDocument();
});
it('should preserve non-scope filters from query prop when changing scope', async () => {
const nonScopeItem = createNonScopeFilter('customTag', 'customValue');
const initialRootFilter = createSpanScopeFilter('isRoot');
const localQuery = createLocalQuery([nonScopeItem, initialRootFilter], 'OR');
const { container } = renderWithContext(
defaultQuery,
mockOnChange,
localQuery,
);
expect(await screen.findByText('Root Spans')).toBeInTheDocument();
await selectOption('Entrypoint Spans');
expect(mockRedirectWithQueryBuilderData).not.toHaveBeenCalled();
assertOnChangePayload(0, 'isEntryPoint', [nonScopeItem]);
expect(
container.querySelector('span[title="Entrypoint Spans"]'),
).toBeInTheDocument();
});
it('should preserve non-scope filters when changing to ALL_SPANS', async () => {
const nonScopeItem1 = createNonScopeFilter('service', 'checkout');
const nonScopeItem2 = createNonScopeFilter('version', 'v1');
const initialEntryFilter = createSpanScopeFilter('isEntryPoint');
const localQuery = createLocalQuery([
nonScopeItem1,
initialEntryFilter,
nonScopeItem2,
]);
const { container } = renderWithContext(
defaultQuery,
mockOnChange,
localQuery,
);
expect(await screen.findByText('Entrypoint Spans')).toBeInTheDocument();
await selectOption('All Spans');
expect(mockRedirectWithQueryBuilderData).not.toHaveBeenCalled();
assertOnChangePayload(0, null, [nonScopeItem1, nonScopeItem2]);
expect(
container.querySelector('span[title="All Spans"]'),
).toBeInTheDocument();
});
});
});

View File

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

View File

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

View File

@@ -5,13 +5,13 @@
flex-direction: row;
align-items: center;
height: 32px;
height: 36px;
margin-bottom: 4px;
cursor: pointer;
&.active {
.nav-item-active-marker {
background: #4e74f8;
background: #3f5ecc;
}
}
@@ -27,24 +27,24 @@
.nav-item-data {
color: white;
background: var(--Slate-500, #161922);
background: #121317;
}
}
&.active {
.nav-item-data {
color: white;
background: var(--Slate-500, #161922);
background: #121317;
// color: #3f5ecc;
}
}
.nav-item-active-marker {
margin: 4px 0;
margin: 8px 0;
width: 8px;
height: 24px;
background: transparent;
border-radius: 2px;
border-radius: 3px;
margin-left: -5px;
}
@@ -53,25 +53,24 @@
max-width: calc(100% - 24px);
display: flex;
margin: 0px 8px;
padding: 2px 8px;
padding: 4px 12px;
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: 300;
font-weight: 400;
line-height: 18px;
background: transparent;
transition: 0.2s all linear;
border-radius: 3px;
.nav-item-icon {
height: 16px;
}
@@ -81,31 +80,6 @@
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;
}
}
}
@@ -118,7 +92,7 @@
.nav-item {
&.active {
.nav-item-active-marker {
background: #4e74f8;
background: #3f5ecc;
}
}
@@ -141,10 +115,6 @@
.nav-item-data {
color: #121317;
.nav-item-label {
color: var(--bg-ink-400);
}
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -56,7 +56,7 @@
gap: 6px;
.diamond {
fill: var(--bg-robin-500);
fill: var(--bg-cherry-500);
}
}
}

View File

@@ -3,8 +3,8 @@ import './Events.styles.scss';
import { Collapse, Input, Tooltip, Typography } from 'antd';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { Diamond } from 'lucide-react';
import { useState } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import { useMemo, useState } from 'react';
import { Event, Span } from 'types/api/trace/getTraceV2';
import NoData from '../NoData/NoData';
@@ -17,7 +17,14 @@ interface IEventsTableProps {
function EventsTable(props: IEventsTableProps): JSX.Element {
const { span, startTime, isSearchVisible } = props;
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
const events = span.event;
const events: Event[] = useMemo(() => {
const tempEvents = [];
for (let i = 0; i < span.event?.length; i++) {
const parsedEvent = JSON.parse(span.event[i]);
tempEvents.push(parsedEvent);
}
return tempEvents;
}, [span.event]);
return (
<div className="events-table">
@@ -74,18 +81,7 @@ function EventsTable(props: IEventsTableProps): JSX.Element {
)}
</Typography.Text>
<Typography.Text className="timestamp-text">
since trace start
</Typography.Text>
</div>
<div className="timestamp-container">
<Typography.Text className="attribute-value">
{getYAxisFormattedValue(
`${(event.timeUnixNano || 0) / 1e6 - span.timestamp}`,
'ms',
)}
</Typography.Text>
<Typography.Text className="timestamp-text">
since span start
after the start
</Typography.Text>
</div>
</div>

View File

@@ -1,71 +0,0 @@
.no-linked-spans {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
.linked-spans-container {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
.item {
display: flex;
flex-direction: column;
gap: 8px;
justify-content: flex-start;
.item-key {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.value-wrapper {
display: flex;
padding: 2px 8px;
align-items: center;
width: fit-content;
max-width: 100%;
gap: 8px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
.item-value {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: 0.56px;
}
}
}
}
.lightMode {
.linked-spans-container {
.item {
.item-key {
color: var(--bg-ink-100);
}
.value-wrapper {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
.item-value {
color: var(--bg-ink-400);
}
}
}
}
}

View File

@@ -1,77 +0,0 @@
import './LinkedSpans.styles.scss';
import { Button, Tooltip, Typography } from 'antd';
import ROUTES from 'constants/routes';
import { formUrlParams } from 'container/TraceDetail/utils';
import { useCallback } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import NoData from '../NoData/NoData';
interface LinkedSpansProps {
span: Span;
}
interface SpanReference {
traceId: string;
spanId: string;
refType: string;
}
function LinkedSpans(props: LinkedSpansProps): JSX.Element {
const { span } = props;
const getLink = useCallback((item: SpanReference): string | null => {
if (!item.traceId || !item.spanId) {
return null;
}
return `${ROUTES.TRACE}/${item.traceId}${formUrlParams({
spanId: item.spanId,
levelUp: 0,
levelDown: 0,
})}`;
}, []);
// Filter out CHILD_OF references as they are parent-child relationships
const linkedSpans =
span.references?.filter((ref: SpanReference) => ref.refType !== 'CHILD_OF') ||
[];
if (linkedSpans.length === 0) {
return (
<div className="no-linked-spans">
<NoData name="linked spans" />
</div>
);
}
return (
<div className="linked-spans-container">
{linkedSpans.map((item: SpanReference) => {
const link = getLink(item);
return (
<div className="item" key={item.spanId}>
<Typography.Text className="item-key" ellipsis>
Linked Span ID
</Typography.Text>
<div className="value-wrapper">
<Tooltip title={item.spanId}>
{link ? (
<Typography.Link href={link} className="item-value" ellipsis>
{item.spanId}
</Typography.Link>
) : (
<Button type="link" className="item-value" disabled>
{item.spanId}
</Button>
)}
</Tooltip>
</div>
</div>
);
})}
</div>
);
}
export default LinkedSpans;

View File

@@ -158,29 +158,20 @@
border-bottom: 1px solid var(--bg-slate-400) !important;
}
.ant-tabs-tab {
margin: 0 !important;
padding: 0 2px !important;
min-width: 36px;
height: 36px;
.attributes-tab-btn {
display: flex;
align-items: center;
justify-content: center;
}
.attributes-tab-btn:hover {
background: unset;
}
.attributes-tab-btn,
.events-tab-btn,
.linked-spans-tab-btn {
.events-tab-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 4px 8px;
}
.attributes-tab-btn:hover,
.events-tab-btn:hover,
.linked-spans-tab-btn:hover {
.events-tab-btn:hover {
background: unset;
}
}
@@ -270,9 +261,3 @@
}
}
}
.linked-spans-tab-btn {
display: flex;
align-items: center;
gap: 0.5rem;
}

View File

@@ -10,14 +10,13 @@ import { getTraceToLogsQuery } from 'container/TraceDetail/SelectedSpanDetails/c
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { Anvil, Bookmark, Link2, PanelRight, Search } from 'lucide-react';
import { Anvil, Bookmark, PanelRight, Search } from 'lucide-react';
import { Dispatch, SetStateAction, useState } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import { formatEpochTimestamp } from 'utils/timeUtils';
import Attributes from './Attributes/Attributes';
import Events from './Events/Events';
import LinkedSpans from './LinkedSpans/LinkedSpans';
const FIVE_MINUTES_IN_MS = 5 * 60 * 1000;
interface ISpanDetailsDrawerProps {
@@ -75,19 +74,6 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
/>
),
},
{
label: (
<Button
type="text"
icon={<Link2 size="14" />}
className="linked-spans-tab-btn"
>
Links
</Button>
),
key: 'linked-spans',
children: <LinkedSpans span={span} />,
},
];
}
const onLogsHandler = (): void => {

View File

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

View File

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

View File

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

View File

@@ -30,15 +30,14 @@ export const getChartData = (
};
const chartLabels: ChartData<'line'>['labels'] = [];
if (allDataPoints && typeof allDataPoints === 'object')
Object.keys(allDataPoints).forEach((timestamp) => {
const key = allDataPoints[timestamp];
if (key.value) {
chartDataset.data.push(key.value);
const date = dayjs(key.timestamp / 1000000);
chartLabels.push(date.toDate().getTime());
}
});
Object.keys(allDataPoints ?? {}).forEach((timestamp) => {
const key = allDataPoints[timestamp];
if (key.value) {
chartDataset.data.push(key.value);
const date = dayjs(key.timestamp / 1000000);
chartLabels.push(date.toDate().getTime());
}
});
return {
datasets: [

View File

@@ -136,12 +136,8 @@ function Filters({
return (
<div className="filter-row">
<QueryBuilderSearchV2
query={{
...BASE_FILTER_QUERY,
filters,
}}
query={BASE_FILTER_QUERY}
onChange={handleFilterChange}
hideSpanScopeSelector={false}
/>
{filteredSpanIds.length > 0 && (
<div className="pre-next-toggle">

View File

@@ -273,27 +273,6 @@
border-radius: 6px;
}
.event-dot {
position: absolute;
top: 50%;
transform: translate(-50%, -50%) rotate(45deg);
width: 6px;
height: 6px;
background-color: var(--bg-robin-500);
border: 1px solid var(--bg-robin-600);
cursor: pointer;
z-index: 1;
&.error {
background-color: var(--bg-cherry-500);
border-color: var(--bg-cherry-600);
}
&:hover {
transform: translate(-50%, -50%) rotate(45deg) scale(1.5);
}
}
.span-line-text {
position: relative;
top: 40%;

View File

@@ -149,28 +149,30 @@ function SpanOverview({
<Typography.Text className="service-name">
{span.serviceName}
</Typography.Text>
{!!span.serviceName && !!span.name && (
<div className="add-funnel-button">
<span className="add-funnel-button__separator">·</span>
<Button
type="text"
size="small"
className="add-funnel-button__button"
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
handleAddSpanToFunnel(span);
}}
icon={
<img
className="add-funnel-button__icon"
src="/Icons/funnel-add.svg"
alt="funnel-icon"
/>
}
/>
</div>
)}
{!!span.serviceName &&
!!span.name &&
process.env.NODE_ENV === 'development' && (
<div className="add-funnel-button">
<span className="add-funnel-button__separator">·</span>
<Button
type="text"
size="small"
className="add-funnel-button__button"
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
handleAddSpanToFunnel(span);
}}
icon={
<img
className="add-funnel-button__icon"
src="/Icons/funnel-add.svg"
alt="funnel-icon"
/>
}
/>
</div>
)}
</section>
</div>
</div>
@@ -238,33 +240,8 @@ export function SpanDuration({
left: `${leftOffset}%`,
width: `${width}%`,
backgroundColor: color,
position: 'relative',
}}
>
{span.event?.map((event) => {
const eventTimeMs = event.timeUnixNano / 1e6;
const eventOffsetPercent =
((eventTimeMs - span.timestamp) / (span.durationNano / 1e6)) * 100;
const clampedOffset = Math.max(1, Math.min(eventOffsetPercent, 99));
const { isError } = event;
const { time, timeUnitName } = convertTimeToRelevantUnit(
eventTimeMs - span.timestamp,
);
return (
<Tooltip
key={`${span.spanId}-event-${event.name}-${event.timeUnixNano}`}
title={`${event.name} @ ${toFixed(time, 2)} ${timeUnitName}`}
>
<div
className={`event-dot ${isError ? 'error' : ''}`}
style={{
left: `${clampedOffset}%`,
}}
/>
</Tooltip>
);
})}
</div>
/>
{hasActionButtons && <SpanLineActionButtons span={span} />}
<Tooltip title={`${toFixed(time, 2)} ${timeUnitName}`}>
<Typography.Text
@@ -473,7 +450,7 @@ function Success(props: ISuccessProps): JSX.Element {
virtualiserRef={virtualizerRef}
setColumnWidths={setTraceFlamegraphStatsWidth}
/>
{selectedSpanToAddToFunnel && (
{selectedSpanToAddToFunnel && process.env.NODE_ENV === 'development' && (
<AddSpanToFunnelModal
span={selectedSpanToAddToFunnel}
isOpen={isAddSpanToFunnelModalOpen}

View File

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

View File

@@ -1,7 +1,5 @@
import './Version.styles.scss';
import { Button, Form } from 'antd';
import { CheckCircle, CloudUpload, InfoIcon, Wrench } from 'lucide-react';
import { WarningFilled } from '@ant-design/icons';
import { Button, Card, Form, Space, Typography } from 'antd';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
@@ -36,82 +34,73 @@ function Version(): JSX.Element {
);
return (
<div className="version-container">
<header className="version-page-header">
<div className="version-page-header-title">
<Wrench size={16} />
Version
<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>
</header>
)}
<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>
)}
{!isError && !isLatestVersion && (
<div>
<Space align="start">
<span>
<WarningFilled style={{ color: '#E87040' }} />
</span>
<Typography.Paragraph italic>{t('stale_version')}</Typography.Paragraph>
</Space>
</div>
</div>
</div>
)}
{!isError && !isLatestVersion && (
<Button
href="https://signoz.io/docs/operate/docker-standalone/#upgrade"
target="_blank"
>
{t('read_how_to_upgrade')}
</Button>
)}
</Card>
);
}

View File

@@ -9,11 +9,6 @@
<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>
@@ -58,17 +53,6 @@
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/uPlot.min.css" />
<% if (htmlWebpackPlugin.options.templateParameters.preloadFonts) { %> <%
htmlWebpackPlugin.options.templateParameters.preloadFonts.forEach(function(font)
{ %>
<link
rel="preload"
href="<%= font.href %>"
as="<%= font.as %>"
type="<%= font.type %>"
crossorigin="<%= font.crossorigin %>"
/>
<% }); %> <% } %>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -9,6 +9,7 @@ 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';
@@ -46,6 +47,9 @@ if (container) {
<AppRoutes />
</AppProvider>
</Provider>
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
</TimezoneProvider>
</ThemeProvider>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import LiveLogsContainer from 'container/LiveLogs/LiveLogsContainer';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { EventSourceProvider } from 'providers/EventSource';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { useEffect } from 'react';
import { DataSource } from 'types/common/queryBuilder';
@@ -18,9 +17,7 @@ function LiveLogs(): JSX.Element {
return (
<EventSourceProvider>
<PreferenceContextProvider>
<LiveLogsContainer />
</PreferenceContextProvider>
<LiveLogsContainer />
</EventSourceProvider>
);
}

View File

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

View File

@@ -8,7 +8,6 @@ import { noop } from 'lodash-es';
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderContext } from 'providers/QueryBuilder';
// https://virtuoso.dev/mocking-in-tests/
import { VirtuosoMockContext } from 'react-virtuoso';
@@ -74,25 +73,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
// Mock usePreferenceSync
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
formatting: {
maxLines: 2,
format: 'table',
fontSize: 'small',
version: 1,
},
},
loading: false,
error: null,
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
}),
}));
const logsQueryServerRequest = (): void =>
server.use(
rest.post(queryRangeURL, (req, res, ctx) =>
@@ -108,11 +88,7 @@ describe('Logs Explorer Tests', () => {
queryByText,
getByTestId,
queryByTestId,
} = render(
<PreferenceContextProvider>
<LogsExplorer />
</PreferenceContextProvider>,
);
} = render(<LogsExplorer />);
// check the presence of frequency chart content
expect(getByText(frequencyChartContent)).toBeInTheDocument();
@@ -138,9 +114,9 @@ describe('Logs Explorer Tests', () => {
expect(timeSeriesView).toBeInTheDocument();
expect(tableView).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();
// check the presence of old logs explorer CTA
const oldLogsCTA = getByText('Switch to Old Logs Explorer');
expect(oldLogsCTA).toBeInTheDocument();
});
// update this test properly
@@ -148,13 +124,11 @@ describe('Logs Explorer Tests', () => {
// mocking the query range API to return the logs
logsQueryServerRequest();
const { queryByText, queryByTestId } = render(
<PreferenceContextProvider>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorer />
</VirtuosoMockContext.Provider>
</PreferenceContextProvider>,
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorer />
</VirtuosoMockContext.Provider>,
);
// check for loading state to be not present
@@ -218,13 +192,11 @@ describe('Logs Explorer Tests', () => {
isStagedQueryUpdated: (): boolean => false,
}}
>
<PreferenceContextProvider>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorer />
</VirtuosoMockContext.Provider>
</PreferenceContextProvider>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorer />
</VirtuosoMockContext.Provider>
</QueryBuilderContext.Provider>,
);
@@ -241,11 +213,7 @@ describe('Logs Explorer Tests', () => {
});
test('frequency chart visibility and switch toggle', async () => {
const { getByRole, queryByText } = render(
<PreferenceContextProvider>
<LogsExplorer />
</PreferenceContextProvider>,
);
const { getByRole, queryByText } = render(<LogsExplorer />);
// check the presence of Frequency Chart
expect(queryByText('Frequency chart')).toBeInTheDocument();

View File

@@ -23,7 +23,6 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { isEqual, isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
@@ -36,8 +35,6 @@ function LogsExplorer(): JSX.Element {
const [selectedView, setSelectedView] = useState<SELECTED_VIEWS>(
SELECTED_VIEWS.SEARCH,
);
const { preferences, loading: preferencesLoading } = usePreferenceContext();
const [showFilters, setShowFilters] = useState<boolean>(() => {
const localStorageValue = getLocalStorageKey(
LOCALSTORAGE.SHOW_LOGS_QUICK_FILTERS,
@@ -86,6 +83,7 @@ function LogsExplorer(): JSX.Element {
}, [currentQuery.builder.queryData, currentQuery.builder.queryData.length]);
const {
queryData: optionsQueryData,
redirectWithQuery: redirectWithOptionsData,
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
@@ -166,34 +164,12 @@ function LogsExplorer(): JSX.Element {
);
useEffect(() => {
if (!preferences || preferencesLoading) {
return;
}
const migratedQuery = migrateOptionsQuery({
selectColumns: preferences.columns || defaultLogsSelectedColumns,
maxLines: preferences.formatting?.maxLines || defaultOptionsQuery.maxLines,
format: preferences.formatting?.format || defaultOptionsQuery.format,
fontSize: preferences.formatting?.fontSize || defaultOptionsQuery.fontSize,
version: preferences.formatting?.version,
});
const migratedQuery = migrateOptionsQuery(optionsQueryData);
// Only redirect if the query was actually modified
if (
!isEqual(migratedQuery, {
selectColumns: preferences?.columns,
maxLines: preferences?.formatting?.maxLines,
format: preferences?.formatting?.format,
fontSize: preferences?.formatting?.fontSize,
version: preferences?.formatting?.version,
})
) {
if (!isEqual(migratedQuery, optionsQueryData)) {
redirectWithOptionsData(migratedQuery);
}
}, [
migrateOptionsQuery,
preferences,
redirectWithOptionsData,
preferencesLoading,
]);
}, [migrateOptionsQuery, optionsQueryData, redirectWithOptionsData]);
const isMultipleQueries = useMemo(
() =>

View File

@@ -4,14 +4,9 @@ import { Compass, TowerControl, Workflow } from 'lucide-react';
import LogsExplorer from 'pages/LogsExplorer';
import Pipelines from 'pages/Pipelines';
import SaveView from 'pages/SaveView';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
export const logsExplorer: TabRoutes = {
Component: (): JSX.Element => (
<PreferenceContextProvider>
<LogsExplorer />
</PreferenceContextProvider>
),
Component: LogsExplorer,
name: (
<div className="tab-item">
<Compass size={16} /> Explorer

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import ExplorerPage from 'container/MetricsExplorer/Explorer';
import SummaryPage from 'container/MetricsExplorer/Summary';
import { BarChart2, Compass, TowerControl } from 'lucide-react';
import SaveView from 'pages/SaveView';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
export const Summary: TabRoutes = {
Component: SummaryPage,
@@ -18,11 +17,7 @@ export const Summary: TabRoutes = {
};
export const Explorer: TabRoutes = {
Component: (): JSX.Element => (
<PreferenceContextProvider>
<ExplorerPage />
</PreferenceContextProvider>
),
Component: ExplorerPage,
name: (
<div className="tab-item">
<Compass size={16} /> Explorer

View File

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

View File

@@ -1,234 +0,0 @@
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;

Some files were not shown because too many files have changed in this diff Show More