mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-09 11:12:21 +00:00
Compare commits
1 Commits
feat/issue
...
v0.87.0-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38b1d92252 |
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export const USER_PREFERENCES = {
|
||||
SIDENAV_PINNED: 'sidenav_pinned',
|
||||
NAV_SHORTCUTS: 'nav_shortcuts',
|
||||
};
|
||||
@@ -21,7 +21,7 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
const [action] = useComponentPermission(['new_alert_action'], user.role);
|
||||
|
||||
const onClickEditHandler = useCallback((id: string) => {
|
||||
history.push(
|
||||
history.replace(
|
||||
generatePath(ROUTES.CHANNELS_EDIT, {
|
||||
id,
|
||||
}),
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.alert-channels-container {
|
||||
width: 90%;
|
||||
margin: 12px auto;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,12 +22,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.side-nav-pinned {
|
||||
.app-content {
|
||||
width: calc(100% - 240px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-support-gateway {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
.billing-container {
|
||||
margin-bottom: 40px;
|
||||
padding-top: 36px;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
width: 65%;
|
||||
|
||||
.billing-summary {
|
||||
margin: 24px 8px;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import styled from 'styled-components';
|
||||
|
||||
export const ApplyFormContainer = styled.div`
|
||||
&&& {
|
||||
padding: 16px;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -298,6 +298,8 @@
|
||||
}
|
||||
|
||||
.onboarding-v2 {
|
||||
margin: 0px -1rem;
|
||||
|
||||
.onboarding-header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1037,9 +1037,7 @@ function QueryBuilderSearchV2(
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
{!hideSpanScopeSelector && (
|
||||
<SpanScopeSelector query={query} onChange={onChange} />
|
||||
)}
|
||||
{!hideSpanScopeSelector && <SpanScopeSelector queryName={query.queryName} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,7 +96,7 @@ function ServiceMetricTable({
|
||||
`${range[0]}-${range[1]} of ${total} items`,
|
||||
};
|
||||
return (
|
||||
<div className="service-metric-table-container">
|
||||
<>
|
||||
{RPS > MAX_RPS_LIMIT && (
|
||||
<Flex justify="left">
|
||||
<Typography.Title level={5} type="warning" style={{ marginTop: 0 }}>
|
||||
@@ -116,7 +116,7 @@ function ServiceMetricTable({
|
||||
rowKey="serviceName"
|
||||
className="service-metrics-table"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ function ServiceTraceTable({
|
||||
`${range[0]}-${range[1]} of ${total} items`,
|
||||
};
|
||||
return (
|
||||
<div className="service-traces-table-container">
|
||||
<>
|
||||
{RPS > MAX_RPS_LIMIT && (
|
||||
<Flex justify="left">
|
||||
<Typography.Title level={5} type="warning" style={{ marginTop: 0 }}>
|
||||
@@ -73,7 +73,7 @@ function ServiceTraceTable({
|
||||
rowKey="serviceName"
|
||||
className="service-traces-table"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
height: 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
gap: 6px;
|
||||
|
||||
.diamond {
|
||||
fill: var(--bg-robin-500);
|
||||
fill: var(--bg-cherry-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.top-nav-container {
|
||||
padding: 0px 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
.alerts-container {
|
||||
.ant-tabs-nav-wrap {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
@@ -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' : ''
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.request-entity-container {
|
||||
margin: 16px 0px;
|
||||
margin: 0;
|
||||
border-right: none;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,11 +59,11 @@ function MetricsApplication(): JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="metrics-application-container">
|
||||
<>
|
||||
<ResourceAttributesFilter />
|
||||
<ApDexApplication />
|
||||
<RouteTab routes={routes} history={history} activeKey={activeKey} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user