Compare commits

...

17 Commits

Author SHA1 Message Date
swapnil-signoz
46642d38fd chore: adding required tags for ServiceDashboard struct 2026-06-02 20:50:14 +05:30
swapnil-signoz
9cbbdf84f3 chore: adding required tags 2026-06-02 19:42:53 +05:30
swapnil-signoz
3056aa9a0c Merge branch 'main' into refactor/cloudintegration-service-details 2026-06-02 19:41:39 +05:30
Vinicius Lourenço
a487b311bc fix(metrics-explorer): handle in case .data is undefined (#11527)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-06-02 13:46:53 +00:00
Vinicius Lourenço
6473066193 fix(metrics-explorer): handle in case .atributes is undefined (#11528) 2026-06-02 13:46:52 +00:00
SagarRajput-7
fb0d34ae35 feat(auth): validate reset password token on page load before showing form (#11522)
* feat(auth): validate reset password token on page load before showing form

* fix(auth): distinct error copy for expired vs invalid token; skip 401 rotation on verify endpoint

* fix(auth): use endsWith for orval-generated endpoint guards in interceptorRejected

* Revert "fix(auth): use endsWith for orval-generated endpoint guards in interceptorRejected"

This reverts commit 00aa23b8fc.
2026-06-02 13:38:18 +00:00
Vinicius Lourenço
ba684acba3 fix(create-alert-v2): tooltip not showing due to pointer-events none (#11489) 2026-06-02 12:05:02 +00:00
Gaurav Tewari
184724003a chore: remove query status (#11476)
* chore: remove confusing query status

* chore: remove extra things

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-06-02 11:48:40 +00:00
Vinicius Lourenço
a4d3f10da8 chore(codeowners): add pulse for alerts and infra monitoring pages (#11508) 2026-06-02 11:44:38 +00:00
swapnil-signoz
1595d80473 Merge branch 'main' into refactor/cloudintegration-service-details 2026-06-02 15:10:22 +05:30
swapnil-signoz
0bcea510e6 refactor: updating nullable tag 2026-06-01 15:28:09 +05:30
swapnil-signoz
6869f2121d Merge branch 'main' into refactor/cloudintegration-service-details 2026-06-01 15:20:18 +05:30
swapnil-signoz
e9bdad84ef refactor: updating response 2026-06-01 15:19:44 +05:30
swapnil-signoz
5933b262b2 revert: restore frontend files to main state 2026-05-29 16:43:52 +05:30
swapnil-signoz
5b942d3b83 Merge branch 'main' into refactor/cloudintegration-service-details 2026-05-29 16:32:05 +05:30
swapnil-signoz
cf85659089 refactor: removing unused enrichDashboardIDs func 2026-05-28 00:06:14 +05:30
swapnil-signoz
528b5e2029 refactor: service response changes and dashboardID handling 2026-05-27 23:49:09 +05:30
22 changed files with 447 additions and 230 deletions

19
.github/CODEOWNERS vendored
View File

@@ -169,3 +169,22 @@ go.mod @therealpandey
## Dashboard V2
/frontend/src/pages/DashboardPageV2/ @SigNoz/pulse-frontend
/frontend/src/pages/DashboardsListPageV2/ @SigNoz/pulse-frontend
## Infrastructure Monitoring
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringHosts/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringK8s/ @SigNoz/pulse-frontend
## Alerts
/frontend/src/pages/AlertList/ @SigNoz/pulse-frontend
/frontend/src/pages/AlertDetails/ @SigNoz/pulse-frontend
/frontend/src/pages/CreateAlert/ @SigNoz/pulse-frontend
/frontend/src/pages/EditRules/ @SigNoz/pulse-frontend
/frontend/src/container/AlertHistory/ @SigNoz/pulse-frontend
/frontend/src/container/CreateAlertRule/ @SigNoz/pulse-frontend
/frontend/src/container/CreateAlertV2/ @SigNoz/pulse-frontend
/frontend/src/container/EditAlertV2/ @SigNoz/pulse-frontend
/frontend/src/container/FormAlertRules/ @SigNoz/pulse-frontend
/frontend/src/container/ListAlertRules/ @SigNoz/pulse-frontend
/frontend/src/container/TriggeredAlerts/ @SigNoz/pulse-frontend
/frontend/src/container/AnomalyAlertEvaluationView/ @SigNoz/pulse-frontend

View File

@@ -870,14 +870,6 @@ components:
- timestampMillis
- data
type: object
CloudintegrationtypesAssets:
properties:
dashboards:
items:
$ref: '#/components/schemas/CloudintegrationtypesDashboard'
nullable: true
type: array
type: object
CloudintegrationtypesAzureAccountConfig:
properties:
deploymentRegion:
@@ -1025,17 +1017,6 @@ components:
- ingestionUrl
- ingestionKey
type: object
CloudintegrationtypesDashboard:
properties:
definition:
$ref: '#/components/schemas/DashboardtypesStorableDashboardData'
description:
type: string
id:
type: string
title:
type: string
type: object
CloudintegrationtypesDataCollected:
properties:
logs:
@@ -1209,7 +1190,7 @@ components:
CloudintegrationtypesService:
properties:
assets:
$ref: '#/components/schemas/CloudintegrationtypesAssets'
$ref: '#/components/schemas/CloudintegrationtypesServiceAssets'
cloudIntegrationService:
$ref: '#/components/schemas/CloudintegrationtypesCloudIntegrationService'
dataCollected:
@@ -1222,8 +1203,6 @@ components:
type: string
supportedSignals:
$ref: '#/components/schemas/CloudintegrationtypesSupportedSignals'
telemetryCollectionStrategy:
$ref: '#/components/schemas/CloudintegrationtypesTelemetryCollectionStrategy'
title:
type: string
required:
@@ -1234,9 +1213,17 @@ components:
- assets
- supportedSignals
- dataCollected
- telemetryCollectionStrategy
- cloudIntegrationService
type: object
CloudintegrationtypesServiceAssets:
properties:
dashboards:
items:
$ref: '#/components/schemas/CloudintegrationtypesServiceDashboard'
type: array
required:
- dashboards
type: object
CloudintegrationtypesServiceConfig:
properties:
aws:
@@ -1244,6 +1231,18 @@ components:
azure:
$ref: '#/components/schemas/CloudintegrationtypesAzureServiceConfig'
type: object
CloudintegrationtypesServiceDashboard:
properties:
description:
type: string
integrationDashboard:
$ref: '#/components/schemas/CloudintegrationtypesStorableIntegrationDashboard'
title:
type: string
required:
- title
- description
type: object
CloudintegrationtypesServiceID:
enum:
- alb
@@ -1278,6 +1277,30 @@ components:
- icon
- enabled
type: object
CloudintegrationtypesStorableIntegrationDashboard:
properties:
createdAt:
format: date-time
type: string
dashboardId:
type: string
id:
type: string
provider:
type: string
slug:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- dashboardId
- provider
- slug
- createdAt
- updatedAt
type: object
CloudintegrationtypesSupportedSignals:
properties:
logs:
@@ -1285,13 +1308,6 @@ components:
metrics:
type: boolean
type: object
CloudintegrationtypesTelemetryCollectionStrategy:
properties:
aws:
$ref: '#/components/schemas/CloudintegrationtypesAWSTelemetryCollectionStrategy'
azure:
$ref: '#/components/schemas/CloudintegrationtypesAzureTelemetryCollectionStrategy'
type: object
CloudintegrationtypesUpdatableAccount:
properties:
config:

View File

@@ -355,26 +355,32 @@ func (module *module) GetService(ctx context.Context, orgID valuer.UUID, service
var integrationService *cloudintegrationtypes.CloudIntegrationService
if !cloudIntegrationID.IsZero() {
storedService, err := module.store.GetServiceByServiceID(ctx, cloudIntegrationID, serviceID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if storedService != nil {
serviceConfig, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, storedService.Config)
if err != nil {
return nil, err
}
integrationService = cloudintegrationtypes.NewCloudIntegrationServiceFromStorable(storedService, serviceConfig)
}
if err := module.enrichDashboardIDs(ctx, orgID, provider, serviceID, serviceDefinition); err != nil {
return nil, err
}
if cloudIntegrationID.IsZero() {
return cloudintegrationtypes.NewService(provider, serviceDefinition, nil, nil), nil
}
return cloudintegrationtypes.NewService(*serviceDefinition, integrationService), nil
storedService, err := module.store.GetServiceByServiceID(ctx, cloudIntegrationID, serviceID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if storedService == nil {
return cloudintegrationtypes.NewService(provider, serviceDefinition, nil, nil), nil
}
serviceConfig, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, storedService.Config)
if err != nil {
return nil, err
}
integrationService = cloudintegrationtypes.NewCloudIntegrationServiceFromStorable(storedService, serviceConfig)
slugPrefix := cloudintegrationtypes.CloudIntegrationDashboardSlugPrefix(provider, serviceID)
integrationDashboards, err := module.store.ListIntegrationDashboardsBySlugPrefix(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slugPrefix)
if err != nil {
return nil, err
}
return cloudintegrationtypes.NewService(provider, serviceDefinition, integrationService, integrationDashboards), nil
}
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
@@ -583,20 +589,3 @@ func (module *module) deprovisionDashboards(ctx context.Context, orgID valuer.UU
}
return nil
}
// enrichDashboardIDs replaces the raw dashboard name in each Dashboard.ID with the provisioned UUID.
// TODO: remove this hack and send idiomatic response to client.
func (module *module) enrichDashboardIDs(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, serviceID cloudintegrationtypes.ServiceID, serviceDefinition *cloudintegrationtypes.ServiceDefinition) error {
for i, d := range serviceDefinition.Assets.Dashboards {
slug := cloudintegrationtypes.CloudIntegrationDashboardSlug(provider, serviceID, d.ID)
row, err := module.store.GetIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
continue
}
return err
}
serviceDefinition.Assets.Dashboards[i].ID = row.DashboardID
}
return nil
}

View File

@@ -120,7 +120,8 @@ export const interceptorRejected = async (
!(
response.config.url === '/sessions' && response.config.method === 'delete'
) &&
response.config.url !== '/authz/check'
response.config.url !== '/authz/check' &&
response.config.url !== '/api/v2/reset_password_tokens/verify'
) {
try {
const accessToken = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN);

View File

@@ -1 +0,0 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4062_7291)" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M7 12.833A5.833 5.833 0 107 1.167a5.833 5.833 0 000 11.666z" fill="#E5484D" stroke="#E5484D"/><path d="M8.75 5.25l-3.5 3.5M5.25 5.25l3.5 3.5" stroke="#121317"/></g><defs><clipPath id="prefix__clip0_4062_7291"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 467 B

View File

@@ -196,7 +196,11 @@ function Footer(): JSX.Element {
</Button>
);
if (alertValidationMessage) {
button = <Tooltip title={alertValidationMessage}>{button}</Tooltip>;
button = (
<Tooltip title={alertValidationMessage}>
<span>{button}</span>
</Tooltip>
);
}
return button;
}, [
@@ -224,7 +228,11 @@ function Footer(): JSX.Element {
</Button>
);
if (alertValidationMessage) {
button = <Tooltip title={alertValidationMessage}>{button}</Tooltip>;
button = (
<Tooltip title={alertValidationMessage}>
<span>{button}</span>
</Tooltip>
);
}
return button;
}, [

View File

@@ -9,8 +9,6 @@ import { useOptionsMenu } from 'container/OptionsMenu';
import { ArrowUp10, Minus } from '@signozhq/icons';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import QueryStatus from './QueryStatus';
function LogsActionsContainer({
listQuery,
selectedPanelType,
@@ -18,10 +16,6 @@ function LogsActionsContainer({
handleToggleFrequencyChart,
orderBy,
setOrderBy,
isFetching,
isLoading,
isError,
isSuccess,
}: {
listQuery: any;
selectedPanelType: PANEL_TYPES;
@@ -29,10 +23,6 @@ function LogsActionsContainer({
handleToggleFrequencyChart: () => void;
orderBy: string;
setOrderBy: (value: string) => void;
isFetching: boolean;
isLoading: boolean;
isError: boolean;
isSuccess: boolean;
}): JSX.Element {
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
@@ -106,17 +96,6 @@ function LogsActionsContainer({
</div>
</>
)}
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
selectedPanelType === PANEL_TYPES.TABLE) && (
<div className="query-stats">
<QueryStatus
loading={isLoading || isFetching}
error={isError}
success={isSuccess}
/>
</div>
)}
</div>
</div>
</div>

View File

@@ -155,40 +155,6 @@
}
}
.query-stats {
display: flex;
align-items: center;
gap: 12px;
align-self: flex-end;
.rows {
color: var(--l2-foreground);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.36px;
}
.divider {
width: 1px;
height: 14px;
background: var(--l3-background);
}
.time {
color: var(--l2-foreground);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.36px;
}
}
.ant-btn {
border: none;
}

View File

@@ -1,4 +0,0 @@
.query-status {
display: flex;
align-items: center;
}

View File

@@ -1,49 +0,0 @@
import React, { useMemo } from 'react';
import { Color } from '@signozhq/design-tokens';
import { LoaderCircle, CircleCheck } from '@signozhq/icons';
import { Spin } from 'antd';
import solidXCircleUrl from '@/assets/Icons/solid-x-circle.svg';
import './QueryStatus.styles.scss';
interface IQueryStatusProps {
loading: boolean;
error: boolean;
success: boolean;
}
export default function QueryStatus(
props: IQueryStatusProps,
): React.ReactElement {
const { loading, error, success } = props;
const content = useMemo((): React.ReactElement => {
if (loading) {
return (
<Spin
spinning
size="small"
indicator={<LoaderCircle className="animate-spin" size="md" />}
/>
);
}
if (error) {
return (
<img
src={solidXCircleUrl}
alt="header"
className="error"
style={{ height: '14px', width: '14px' }}
/>
);
}
if (success) {
return (
<CircleCheck className="success" size={14} fill={Color.BG_ROBIN_500} />
);
}
return <div />;
}, [error, loading, success]);
return <div className="query-status">{content}</div>;
}

View File

@@ -160,7 +160,7 @@ function LogsExplorerViewsContainer({
'custom',
);
const { data, isLoading, isFetching, isError, isSuccess, error } =
const { data, isLoading, isFetching, isError, error } =
useGetExplorerQueryRange(
requestData,
selectedPanelType,
@@ -437,10 +437,6 @@ function LogsExplorerViewsContainer({
handleToggleFrequencyChart={handleToggleFrequencyChart}
orderBy={orderBy}
setOrderBy={setOrderBy}
isFetching={isFetching}
isLoading={isLoading}
isError={isError}
isSuccess={isSuccess}
/>
)}

View File

@@ -59,7 +59,7 @@ function AllAttributes({
);
const attributes = useMemo(
() => attributesData?.data.attributes ?? [],
() => attributesData?.data?.attributes ?? [],
[attributesData],
);

View File

@@ -56,7 +56,7 @@ function MetricDetails({
);
const metadata = useMemo(() => {
if (!metricMetadataResponse) {
if (!metricMetadataResponse?.data) {
return null;
}
const { type, description, unit, temporality, isMonotonic } =

View File

@@ -21,6 +21,10 @@
justify-content: center;
margin-bottom: 8px;
color: var(--semantic-primary-foreground);
&--error {
color: var(--destructive);
}
}
.reset-password-header-title {

View File

@@ -0,0 +1,67 @@
import { CircleAlert } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import AuthError from 'components/AuthError/AuthError';
import AuthPageContainer from 'components/AuthPageContainer';
import APIError from 'types/api/error';
import './ResetPassword.styles.scss';
interface TokenErrorContent {
title: string;
subtitle: string;
}
function getErrorContent(error?: APIError): TokenErrorContent {
const code = error?.getErrorCode();
if (code === 'reset_password_token_expired') {
return {
title: 'Reset Password token is expired',
subtitle:
'Password reset links are single-use and expire after a set period. Please request a new password reset link.',
};
}
if (code === 'reset_password_token_not_found') {
return {
title: 'Invalid Reset Link',
subtitle:
'This reset password link is invalid or has already been used. Please request a new password reset link.',
};
}
return {
title: 'Reset Link Unavailable',
subtitle:
'We could not validate your reset password link. Please request a new one.',
};
}
interface TokenErrorProps {
error?: APIError;
}
function TokenError({ error }: TokenErrorProps): JSX.Element {
const { title, subtitle } = getErrorContent(error);
return (
<AuthPageContainer>
<div className="reset-password-card reset-password-card--centered">
<div className="reset-password-header">
<div className="reset-password-header-icon reset-password-header-icon--error">
<CircleAlert size={32} />
</div>
<Typography.Title level={4} className="reset-password-header-title">
{title}
</Typography.Title>
<Typography.Text className="reset-password-header-subtitle">
{subtitle}
</Typography.Text>
</div>
{error && <AuthError error={error} />}
</div>
</AuthPageContainer>
);
}
export default TokenError;

View File

@@ -1,4 +1,3 @@
import { Logout } from 'api/utils';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { rest, server } from 'mocks-server/server';
@@ -17,10 +16,6 @@ jest.mock('lib/history', () => ({
},
}));
jest.mock('api/utils', () => ({
Logout: jest.fn(),
}));
const mockSuccessNotification = jest.fn();
const mockErrorNotification = jest.fn();
@@ -70,17 +65,6 @@ describe('ResetPassword Component', () => {
).toBeInTheDocument();
expect(screen.getByText(/signoz 1\.0\.0/i)).toBeInTheDocument();
});
it('redirects to login when token is missing', () => {
window.history.pushState({}, '', '/password-reset');
render(<ResetPassword version="1.0.0" />, undefined, {
initialRoute: '/password-reset',
});
expect(Logout).toHaveBeenCalled();
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
});
});
describe('Form Validation', () => {

View File

@@ -1,11 +1,10 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-use';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import { Form, Input as AntdInput } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Logout } from 'api/utils';
import resetPasswordApi from 'api/v1/factor_password/resetPassword';
import AuthError from 'components/AuthError/AuthError';
import AuthPageContainer from 'components/AuthPageContainer';
@@ -38,13 +37,6 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
const { notifications } = useNotifications();
const [form] = Form.useForm<FormValues>();
useEffect(() => {
if (!token) {
Logout();
history.push(ROUTES.LOGIN);
}
}, [token]);
const handleFormSubmit: () => Promise<void> = async () => {
try {
setLoading(true);

View File

@@ -0,0 +1,155 @@
import { Logout } from 'api/utils';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { createErrorResponse, rest, server } from 'mocks-server/server';
import { render, screen, waitFor } from 'tests/test-utils';
import ResetPassword from '../index';
jest.mock('lib/history', () => ({
__esModule: true,
default: {
push: jest.fn(),
location: { search: '' },
},
}));
jest.mock('api/utils', () => ({
Logout: jest.fn().mockResolvedValue(undefined),
}));
const VERIFY_TOKEN_ENDPOINT = '*/api/v2/reset_password_tokens/verify';
const VERSION_ENDPOINT = '*/version';
const mockHistoryPush = history.push as jest.MockedFunction<
typeof history.push
>;
const successVerifyResponse = {
data: { id: 'token-id', token: 'valid-token' },
};
const successVersionResponse = {
version: '0.0.1',
ee: 'Y',
setupCompleted: true,
};
describe('ResetPassword Page', () => {
beforeEach(() => {
jest.clearAllMocks();
server.use(
rest.get(VERSION_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(successVersionResponse)),
),
);
});
afterEach(() => {
server.resetHandlers();
});
describe('Token validation on page load', () => {
it('shows spinner then form when token is valid', async () => {
server.use(
rest.post(VERIFY_TOKEN_ENDPOINT, (_, res, ctx) =>
res(ctx.delay(50), ctx.status(200), ctx.json(successVerifyResponse)),
),
);
window.history.pushState({}, '', '/password-reset?token=valid-token');
render(<ResetPassword />, undefined, {
initialRoute: '/password-reset?token=valid-token',
});
// Loading state: spinner visible, form and error absent
expect(screen.getByRole('img', { name: /loading/i })).toBeInTheDocument();
expect(screen.queryByTestId('password')).not.toBeInTheDocument();
expect(
screen.queryByText(/reset password token is expired/i),
).not.toBeInTheDocument();
// After verification resolves: form is shown
await waitFor(() => {
expect(screen.getByTestId('password')).toBeInTheDocument();
});
expect(screen.getByTestId('confirmPassword')).toBeInTheDocument();
});
it('shows "Invalid Reset Link" when token is not found (404)', async () => {
server.use(
rest.post(
VERIFY_TOKEN_ENDPOINT,
createErrorResponse(
404,
'reset_password_token_not_found',
'reset password token does not exist',
),
),
);
window.history.pushState({}, '', '/password-reset?token=invalid-token');
render(<ResetPassword />, undefined, {
initialRoute: '/password-reset?token=invalid-token',
});
await waitFor(() => {
expect(screen.getByText(/invalid reset link/i)).toBeInTheDocument();
});
expect(
screen.getByText(/invalid or has already been used/i),
).toBeInTheDocument();
expect(
screen.getByText(/reset password token does not exist/i),
).toBeInTheDocument();
});
it('shows "token is expired" when token is expired (401) without redirecting to login', async () => {
server.use(
rest.post(
VERIFY_TOKEN_ENDPOINT,
createErrorResponse(
401,
'reset_password_token_expired',
'reset password token has expired',
),
),
);
window.history.pushState({}, '', '/password-reset?token=expired-token');
render(<ResetPassword />, undefined, {
initialRoute: '/password-reset?token=expired-token',
});
await waitFor(() => {
expect(
screen.getByText(/reset password token is expired/i),
).toBeInTheDocument();
});
expect(
screen.getByText(/single-use and expire after a set period/i),
).toBeInTheDocument();
expect(
screen.getByText(/reset password token has expired/i),
).toBeInTheDocument();
// 401 from this endpoint must NOT trigger logout/redirect
expect(mockHistoryPush).not.toHaveBeenCalledWith(ROUTES.LOGIN);
expect(Logout).not.toHaveBeenCalled();
});
it('redirects to login when no token is in the URL', async () => {
window.history.pushState({}, '', '/password-reset');
render(<ResetPassword />, undefined, {
initialRoute: '/password-reset',
});
await waitFor(() => {
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
});
expect(Logout).toHaveBeenCalled();
});
});
});

View File

@@ -1,8 +1,17 @@
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { useLocation } from 'react-use';
import { AxiosError } from 'axios';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import getUserVersion from 'api/v1/version/get';
import { verifyResetPasswordToken } from 'api/generated/services/users';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { Logout } from 'api/utils';
import Spinner from 'components/Spinner';
import ResetPasswordContainer from 'container/ResetPassword';
import TokenError from 'container/ResetPassword/TokenError';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
@@ -10,24 +19,65 @@ import APIError from 'types/api/error';
function ResetPassword(): JSX.Element {
const { user, isLoggedIn } = useAppContext();
const { showErrorModal } = useErrorModal();
const { search } = useLocation();
const params = new URLSearchParams(search || '');
const token = params.get('token') || '';
const { data, isLoading, error } = useQuery({
useEffect(() => {
if (!token) {
void Logout();
history.push(ROUTES.LOGIN);
}
}, [token]);
const {
data: versionData,
isLoading: isVersionLoading,
error: versionError,
} = useQuery({
queryFn: getUserVersion,
queryKey: ['getUserVersion', user?.accessJwt],
enabled: !isLoggedIn,
});
useEffect(() => {
if (error) {
showErrorModal(error as APIError);
}
}, [error, showErrorModal]);
const {
isLoading: isVerifying,
isError: isTokenError,
error: tokenError,
} = useQuery<
Awaited<ReturnType<typeof verifyResetPasswordToken>>,
AxiosError<RenderErrorResponseDTO>
>({
queryFn: () => verifyResetPasswordToken({ token }),
queryKey: ['verifyResetPasswordToken', token],
enabled: !!token,
retry: false,
});
if (isLoading) {
const tokenApiError = useMemo(
() => convertToApiError(tokenError),
[tokenError],
);
useEffect(() => {
if (versionError) {
showErrorModal(versionError as APIError);
}
}, [versionError, showErrorModal]);
if (!token) {
return <Spinner tip="Loading..." />;
}
return <ResetPasswordContainer version={data?.data.version || ''} />;
if (isVersionLoading || isVerifying) {
return <Spinner tip="Validating your reset password token..." />;
}
if (isTokenError) {
return <TokenError error={tokenApiError} />;
}
return <ResetPasswordContainer version={versionData?.data.version || ''} />;
}
export default ResetPassword;

View File

@@ -440,4 +440,3 @@ func (handler *handler) AgentCheckIn(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusOK, cloudintegrationtypes.NewGettableAgentCheckIn(provider, resp))
}

View File

@@ -18,14 +18,16 @@ var (
type StorableIntegrationDashboard struct {
bun.BaseModel `bun:"table:integration_dashboard"`
ID string `bun:"id,pk,type:text"`
DashboardID string `bun:"dashboard_id,type:text"`
Provider IntegrationDashboardProviderType `bun:"provider,type:text"`
Slug string `bun:"slug,type:text"`
CreatedAt time.Time `bun:"created_at"`
UpdatedAt time.Time `bun:"updated_at"`
ID string `json:"id" bun:"id,pk,type:text" required:"true"`
DashboardID string `json:"dashboardId" bun:"dashboard_id,type:text" required:"true"`
Provider IntegrationDashboardProviderType `json:"provider" bun:"provider,type:text" required:"true"`
Slug string `json:"slug" bun:"slug,type:text" required:"true"`
CreatedAt time.Time `json:"createdAt" bun:"created_at" required:"true"`
UpdatedAt time.Time `json:"updatedAt" bun:"updated_at" required:"true"`
}
type IntegrationDashboard = StorableIntegrationDashboard
func NewStorableIntegrationDashboard(dashboardID string, provider IntegrationDashboardProviderType, slug string) *StorableIntegrationDashboard {
now := time.Now()
return &StorableIntegrationDashboard{

View File

@@ -50,10 +50,18 @@ type ListServicesMetadataParams struct {
// Service represents a cloud integration service with its definition,
// cloud integration service is non nil only when the service entry exists in DB with ANY config (enabled or disabled).
type Service struct {
ServiceDefinition
ServiceDefinitionMetadata
Overview string `json:"overview" required:"true"` // markdown
ServiceAssets ServiceAssets `json:"assets" required:"true"`
SupportedSignals SupportedSignals `json:"supportedSignals" required:"true"`
DataCollected DataCollected `json:"dataCollected" required:"true"`
CloudIntegrationService *CloudIntegrationService `json:"cloudIntegrationService" required:"true" nullable:"true"`
}
type ServiceAssets struct {
Dashboards []*ServiceDashboard `json:"dashboards" required:"true" nullable:"false"`
}
type GetServiceParams struct {
CloudIntegrationID valuer.UUID `query:"cloud_integration_id" required:"false"`
}
@@ -121,6 +129,12 @@ type Dashboard struct {
Definition dashboardtypes.StorableDashboardData `json:"definition,omitempty"`
}
type ServiceDashboard struct {
Title string `json:"title" required:"true"`
Description string `json:"description" required:"true"`
IntegrationDashboard *IntegrationDashboard `json:"integrationDashboard,omitempty" required:"false"`
}
func NewCloudIntegrationService(serviceID ServiceID, cloudIntegrationID valuer.UUID, provider CloudProviderType, config *ServiceConfig) (*CloudIntegrationService, error) {
switch provider {
case CloudProviderTypeAWS:
@@ -164,11 +178,41 @@ func NewServiceMetadata(definition ServiceDefinition, enabled bool) *ServiceMeta
}
}
func NewService(def ServiceDefinition, storableService *CloudIntegrationService) *Service {
return &Service{
ServiceDefinition: def,
CloudIntegrationService: storableService,
func NewService(provider CloudProviderType, def *ServiceDefinition, integrationService *CloudIntegrationService, integrationDashboards []*StorableIntegrationDashboard) *Service {
service := &Service{
ServiceDefinitionMetadata: def.ServiceDefinitionMetadata,
Overview: def.Overview,
SupportedSignals: def.SupportedSignals,
DataCollected: def.DataCollected,
CloudIntegrationService: integrationService,
ServiceAssets: ServiceAssets{Dashboards: make([]*ServiceDashboard, 0, len(def.Assets.Dashboards))},
}
integrationDashboardsMap := make(map[string]*IntegrationDashboard)
for _, d := range integrationDashboards {
integrationDashboardsMap[d.Slug] = d
}
for _, d := range def.Assets.Dashboards {
dashboard := &ServiceDashboard{
Title: d.Title,
Description: d.Description,
}
if integrationService != nil {
slug := CloudIntegrationDashboardSlug(provider, integrationService.Type, d.ID)
if integrationDashboard, exists := integrationDashboardsMap[slug]; exists {
if integrationDashboard != nil {
dashboard.IntegrationDashboard = integrationDashboard
}
}
}
service.ServiceAssets.Dashboards = append(service.ServiceAssets.Dashboards, dashboard)
}
return service
}
func NewGettableServicesMetadata(services []*ServiceMetadata) *GettableServicesMetadata {