mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-03 00:30:47 +01:00
Compare commits
2 Commits
keystone-p
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0aeef0dc46 | ||
|
|
7663ab02d4 |
@@ -155,6 +155,39 @@ describe('ResetPassword Component', () => {
|
||||
{ timeout: 200 },
|
||||
);
|
||||
});
|
||||
|
||||
it('clears password mismatch error when confirm password is emptied', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<ResetPassword version="1.0.0" />, undefined, {
|
||||
initialRoute: '/password-reset?token=reset-token-123',
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /reset password/i,
|
||||
});
|
||||
|
||||
await user.type(passwordInput, 'password123');
|
||||
await user.type(confirmPasswordInput, 'password456');
|
||||
await user.tab();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/passwords don't match/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.clear(confirmPasswordInput);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/passwords don't match/i),
|
||||
).not.toBeInTheDocument();
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Successful Password Reset', () => {
|
||||
@@ -288,6 +321,65 @@ describe('ResetPassword Component', () => {
|
||||
expect(screen.queryByText(/invalid token/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('clears API error when the user edits the password', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(RESET_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(400),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'invalid_password',
|
||||
message: 'password must be at least 8 characters long',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ResetPassword version="1.0.0" />, undefined, {
|
||||
initialRoute: '/password-reset?token=reset-token-123',
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /reset password/i,
|
||||
});
|
||||
|
||||
await user.type(passwordInput, 'weak');
|
||||
await user.type(confirmPasswordInput, 'weak');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/password must be at least 8 characters long/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(passwordInput, 'Er1!');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/password must be at least 8 characters long/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.type(confirmPasswordInput, 'Er1!');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading States', () => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useResetPassword } from 'api/generated/services/users';
|
||||
import AuthError from 'components/AuthError/AuthError';
|
||||
import AuthPageContainer from 'components/AuthPageContainer';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight, CircleAlert, KeyRound } from '@signozhq/icons';
|
||||
@@ -23,10 +22,7 @@ import './ResetPassword.styles.scss';
|
||||
type FormValues = { password: string; confirmPassword: string };
|
||||
|
||||
function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
const [confirmPasswordError, setConfirmPasswordError] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [isValidPassword, setIsValidPassword] = useState(false);
|
||||
const [confirmPasswordTouched, setConfirmPasswordTouched] = useState(false);
|
||||
const { t } = useTranslation(['common']);
|
||||
const { search } = useLocation();
|
||||
const params = new URLSearchParams(search);
|
||||
@@ -37,6 +33,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
mutate: resetPassword,
|
||||
isLoading,
|
||||
error: mutationError,
|
||||
reset: resetMutation,
|
||||
} = useResetPassword();
|
||||
|
||||
const errorMessage = useMemo(
|
||||
@@ -45,11 +42,30 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
);
|
||||
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const handleFormSubmit = (): void => {
|
||||
const { password } = form.getFieldsValue();
|
||||
const password = Form.useWatch('password', form);
|
||||
const confirmPassword = Form.useWatch('confirmPassword', form);
|
||||
|
||||
const isPasswordMismatch =
|
||||
Boolean(confirmPassword) && password !== confirmPassword;
|
||||
const showPasswordMismatchError = confirmPasswordTouched && isPasswordMismatch;
|
||||
const isValidPassword =
|
||||
Boolean(password?.trim()) &&
|
||||
Boolean(confirmPassword?.trim()) &&
|
||||
password === confirmPassword;
|
||||
|
||||
const handleValuesChange = (): void => {
|
||||
if (mutationError) {
|
||||
resetMutation();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (): void => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetPassword(
|
||||
{ data: { password, token: token || '' } },
|
||||
{ data: { password: form.getFieldValue('password'), token } },
|
||||
{
|
||||
onSuccess: (): void => {
|
||||
notifications.success({
|
||||
@@ -63,80 +79,6 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
);
|
||||
};
|
||||
|
||||
const validatePassword = (): boolean => {
|
||||
const { password, confirmPassword } = form.getFieldsValue();
|
||||
|
||||
if (
|
||||
password &&
|
||||
confirmPassword &&
|
||||
password.trim() &&
|
||||
confirmPassword.trim() &&
|
||||
password.length > 0 &&
|
||||
confirmPassword.length > 0
|
||||
) {
|
||||
return password === confirmPassword;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleValuesChange = useDebouncedFn((): void => {
|
||||
const { password, confirmPassword } = form.getFieldsValue();
|
||||
|
||||
if (!password || !confirmPassword) {
|
||||
setIsValidPassword(false);
|
||||
}
|
||||
|
||||
// Only clear error if passwords match while typing (but don't set error until blur)
|
||||
if (
|
||||
password &&
|
||||
confirmPassword &&
|
||||
password.trim() &&
|
||||
confirmPassword.trim()
|
||||
) {
|
||||
const isValid = validatePassword();
|
||||
setIsValidPassword(isValid);
|
||||
|
||||
// Only clear error if passwords match, don't set error on mismatch
|
||||
if (isValid) {
|
||||
setConfirmPasswordError(false);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const handlePasswordBlur = (): void => {
|
||||
const { confirmPassword } = form.getFieldsValue();
|
||||
// Only validate if confirm password has a value
|
||||
if (confirmPassword && confirmPassword.trim()) {
|
||||
const isValid = validatePassword();
|
||||
setIsValidPassword(isValid);
|
||||
setConfirmPasswordError(!isValid);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmPasswordBlur = (): void => {
|
||||
const { password, confirmPassword } = form.getFieldsValue();
|
||||
if (
|
||||
password &&
|
||||
password.trim() &&
|
||||
confirmPassword &&
|
||||
confirmPassword.trim()
|
||||
) {
|
||||
const isValid = validatePassword();
|
||||
setIsValidPassword(isValid);
|
||||
setConfirmPasswordError(!isValid);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (): void => {
|
||||
const isValid = validatePassword();
|
||||
setIsValidPassword(isValid);
|
||||
|
||||
if (token) {
|
||||
handleFormSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthPageContainer>
|
||||
<div className="reset-password-card">
|
||||
@@ -158,6 +100,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
<FormContainer
|
||||
form={form}
|
||||
onFinish={handleSubmit}
|
||||
onValuesChange={handleValuesChange}
|
||||
className="reset-password-form"
|
||||
>
|
||||
<div className="reset-password-form-container">
|
||||
@@ -171,8 +114,6 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
>
|
||||
<AntdInput.Password
|
||||
tabIndex={0}
|
||||
onChange={handleValuesChange}
|
||||
onBlur={handlePasswordBlur}
|
||||
id="password"
|
||||
data-testid="password"
|
||||
placeholder="Enter new password"
|
||||
@@ -189,8 +130,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
rules={[{ required: true, message: 'Please enter confirm password!' }]}
|
||||
>
|
||||
<AntdInput.Password
|
||||
onChange={handleValuesChange}
|
||||
onBlur={handleConfirmPasswordBlur}
|
||||
onBlur={(): void => setConfirmPasswordTouched(true)}
|
||||
id="confirmPassword"
|
||||
data-testid="confirmPassword"
|
||||
placeholder="Confirm your new password"
|
||||
@@ -201,7 +141,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{confirmPasswordError && (
|
||||
{showPasswordMismatchError && (
|
||||
<Callout
|
||||
type="error"
|
||||
size="small"
|
||||
@@ -213,7 +153,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
{errorMessage && !confirmPasswordError && (
|
||||
{errorMessage && !showPasswordMismatchError && (
|
||||
<AuthError error={errorMessage} />
|
||||
)}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ var (
|
||||
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
|
||||
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
|
||||
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
|
||||
FeatureJSONBodyDualIngestion = featuretypes.MustNewName("json_body_dual_ingestion")
|
||||
FeatureEnableMetricsReduction = featuretypes.MustNewName("enable_metrics_reduction")
|
||||
FeatureResolveSemconvFamilies = featuretypes.MustNewName("resolve_semconv_families")
|
||||
FeatureUseTraceAttributesJSON = featuretypes.MustNewName("use_trace_attributes_json")
|
||||
@@ -64,6 +65,14 @@ func MustNewRegistry() featuretypes.Registry {
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureJSONBodyDualIngestion,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether the collector's normalize operator keeps the original log body so it is ingested into both the legacy body and the JSON body columns",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureEnableMetricsReduction,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
|
||||
@@ -220,7 +220,26 @@ func (ic *LogParsingPipelineController) ValidatePipelines(ctx context.Context,
|
||||
return err
|
||||
}
|
||||
|
||||
func (ic *LogParsingPipelineController) getNormalizePipeline() pipelinetypes.GettablePipeline {
|
||||
// withNormalizePipeline places normalize where the read path dictates. Ahead of user pipelines
|
||||
// when queries run on body_v2 (use_json_body), so operators see the body the explorer shows.
|
||||
// After them when dual ingestion alone writes body_v2, so operators keep seeing the raw body
|
||||
// users still query. Absent when neither flag is on.
|
||||
func (ic *LogParsingPipelineController) withNormalizePipeline(ctx context.Context, orgID valuer.UUID, pipelines []pipelinetypes.GettablePipeline) []pipelinetypes.GettablePipeline {
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
dualIngestion := ic.fl.BooleanOrEmpty(ctx, flagger.FeatureJSONBodyDualIngestion, evalCtx)
|
||||
switch {
|
||||
case ic.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, evalCtx):
|
||||
return append([]pipelinetypes.GettablePipeline{getNormalizePipeline(dualIngestion)}, pipelines...)
|
||||
case dualIngestion:
|
||||
return append(slices.Clone(pipelines), getNormalizePipeline(true))
|
||||
default:
|
||||
return pipelines
|
||||
}
|
||||
}
|
||||
|
||||
// stashOriginalBody makes normalize carry the pre-normalization body in an internal attribute
|
||||
// for the exporter to restore into the legacy body column.
|
||||
func getNormalizePipeline(stashOriginalBody bool) pipelinetypes.GettablePipeline {
|
||||
return pipelinetypes.GettablePipeline{
|
||||
StoreablePipeline: pipelinetypes.StoreablePipeline{
|
||||
Name: "Default Pipeline - PreProcessing Body",
|
||||
@@ -239,10 +258,11 @@ func (ic *LogParsingPipelineController) getNormalizePipeline() pipelinetypes.Get
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
Type: "normalize",
|
||||
Enabled: true,
|
||||
If: "body != nil",
|
||||
ID: uuid.NewString(),
|
||||
Type: "normalize",
|
||||
Enabled: true,
|
||||
If: "body != nil",
|
||||
JSONBodyDualIngestion: stashOriginalBody,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -351,8 +371,11 @@ func (ic *LogParsingPipelineController) PreviewLogsPipelines(
|
||||
}
|
||||
|
||||
// The collector gets the same pipeline prepended over opamp; see RecommendAgentConfig.
|
||||
// Under dual ingestion alone it runs after user operators and only feeds body_v2, which
|
||||
// the explorer does not show yet, so the preview leaves it out. The original-body stash
|
||||
// is left off: the preview has no exporter to restore and strip it.
|
||||
if ic.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID)) {
|
||||
pipelines = append([]pipelinetypes.GettablePipeline{ic.getNormalizePipeline()}, pipelines...)
|
||||
pipelines = append([]pipelinetypes.GettablePipeline{getNormalizePipeline(false)}, pipelines...)
|
||||
}
|
||||
|
||||
result, collectorLogs, err := SimulatePipelinesProcessing(ctx, pipelines, request.Logs)
|
||||
@@ -373,16 +396,16 @@ func (pc *LogParsingPipelineController) AgentFeatureType() agentConf.AgentFeatur
|
||||
|
||||
// Implements agentConf.AgentFeature interface.
|
||||
// RecommendAgentConfig generates the collector config to be sent to agents.
|
||||
// The normalize pipeline (when use_json_body feature flag is on) is injected here, after
|
||||
// rawPipelineData is serialized. So it is only present in the config sent to
|
||||
// The normalize pipeline (when use_json_body or json_body_dual_ingestion is on) is placed
|
||||
// here, after rawPipelineData is serialized. So it is only present in the config sent to
|
||||
// the collector and never persisted to the database as part of the user's pipeline list.
|
||||
//
|
||||
// NOTE: The configId sent to agents is derived from the pipeline version number
|
||||
// (e.g. "LogPipelines:5"), not the YAML content. If server-side logic changes
|
||||
// the generated YAML without bumping the version (e.g. toggling the use_json_body
|
||||
// flag or updating operator IfExpressions), agents that already applied that version will
|
||||
// not re-apply the new config. In such cases, users must save a new pipeline version
|
||||
// via the API to force agents to pick up the change.
|
||||
// the generated YAML without bumping the version (e.g. toggling the use_json_body or
|
||||
// json_body_dual_ingestion flags or updating operator IfExpressions), agents that already
|
||||
// applied that version will not re-apply the new config. In such cases, users must save a
|
||||
// new pipeline version via the API to force agents to pick up the change.
|
||||
func (pc *LogParsingPipelineController) RecommendAgentConfig(
|
||||
orgId valuer.UUID,
|
||||
currentConfYaml []byte,
|
||||
@@ -408,10 +431,8 @@ func (pc *LogParsingPipelineController) RecommendAgentConfig(
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if pc.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgId)) {
|
||||
// add default normalize pipeline at the beginning, only for sending to collector
|
||||
enrichedPipelines = append([]pipelinetypes.GettablePipeline{pc.getNormalizePipeline()}, enrichedPipelines...)
|
||||
}
|
||||
// normalize is only for sending to the collector, never persisted
|
||||
enrichedPipelines = pc.withNormalizePipeline(ctx, orgId, enrichedPipelines)
|
||||
|
||||
updatedConf, err := GenerateCollectorConfigWithPipelines(currentConfYaml, enrichedPipelines)
|
||||
if err != nil {
|
||||
|
||||
@@ -122,6 +122,9 @@ type PipelineOperator struct {
|
||||
EnablePaths bool `json:"enable_paths,omitempty" yaml:"enable_paths,omitempty"`
|
||||
PathPrefix string `json:"path_prefix,omitempty" yaml:"path_prefix,omitempty"`
|
||||
|
||||
// normalize fields, set by the server only
|
||||
JSONBodyDualIngestion bool `json:"-" yaml:"json_body_dual_ingestion,omitempty"`
|
||||
|
||||
// Used in Severity Parsing and JSON Flattening mapping
|
||||
Mapping map[string][]string `json:"mapping,omitempty" yaml:"mapping,omitempty"`
|
||||
// severity parser fields
|
||||
|
||||
Reference in New Issue
Block a user