Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion static/app/components/workflowEngine/layout/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function Actions({children}: RequiredChildren) {

function HeaderFields({children}: RequiredChildren) {
return (
<Stack gap="xl" column="1 / -1">
<Stack gap="md" column="1 / -1">
{children}
</Stack>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {EditableText} from 'sentry/components/editableText';
import {FormField} from 'sentry/components/forms/formField';
import * as Layout from 'sentry/components/layouts/thirds';
import {t} from 'sentry/locale';
import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context';

export function DetectorNameField() {
const {setHasSetDetectorName} = useDetectorFormContext();

return (
<Layout.Title>
<FormField name="name" inline={false} flexibleControlStateSize stacked>
{({onChange, value}) => (
<EditableText
allowEmpty
value={value || ''}
onChange={newValue => {
onChange(newValue, {
target: {
value: newValue,
},
});
setHasSetDetectorName(true);
}}
placeholder={t('New Monitor')}
aria-label={t('Monitor Name')}
/>
)}
</FormField>
</Layout.Title>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import styled from '@emotion/styled';

import {SelectField} from 'sentry/components/forms/fields/selectField';
import {useFormField} from 'sentry/components/workflowEngine/form/useFormField';
import {t} from 'sentry/locale';
import {useProjects} from 'sentry/utils/useProjects';

export interface EnvironmentConfig {
fieldProps?: Partial<React.ComponentProps<typeof SelectField>>;
includeAllEnvironments?: boolean;
}

type EnvironmentFieldProps = Partial<React.ComponentProps<typeof SelectField>> & {
includeAllEnvironments?: boolean;
};

export function EnvironmentField({
includeAllEnvironments = true,
...props
}: EnvironmentFieldProps) {
const {projects} = useProjects();
const projectId = useFormField<string>('projectId')!;

const environments = projects.find(p => p.id === projectId)?.environments ?? [];

return (
<StyledEnvironmentField
choices={[
...(includeAllEnvironments ? [['', t('All Environments')] as const] : []),
...(environments?.map(environment => [environment, environment] as const) ?? []),
]}
inline={false}
flexibleControlStateSize
stacked
name="environment"
label={t('Environment')}
placeholder={t('Environment')}
aria-label={t('Select Environment')}
size="sm"
{...props}
/>
);
}

const StyledEnvironmentField = styled(SelectField)`
flex-grow: 1;
max-width: 260px;
padding: 0;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {Flex} from '@sentry/scraps/layout';

import {Container} from 'sentry/components/workflowEngine/ui/container';
import {Section} from 'sentry/components/workflowEngine/ui/section';
import {t} from 'sentry/locale';
import {
EnvironmentField,
type EnvironmentConfig,
} from 'sentry/views/detectors/components/forms/common/environmentField';
import {ProjectField} from 'sentry/views/detectors/components/forms/common/projectField';

export type {EnvironmentConfig};

interface ProjectEnvironmentSectionProps {
environment?: EnvironmentConfig;
}

export function ProjectEnvironmentSection({environment}: ProjectEnvironmentSectionProps) {
const environmentConfig = {
includeAllEnvironments: true,
...environment,
};

return (
<Container>
<Section
title={t('Choose the Project and Environment')}
description={t('This is where issues will be created.')}
>
<Flex gap="md">
<ProjectField />
<EnvironmentField
includeAllEnvironments={environmentConfig.includeAllEnvironments}
{...(environmentConfig.fieldProps ?? {})}
/>
</Flex>
</Section>
</Container>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import styled from '@emotion/styled';

import {SentryProjectSelectorField} from 'sentry/components/forms/fields/sentryProjectSelectorField';
import {t} from 'sentry/locale';
import {useProjects} from 'sentry/utils/useProjects';
import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context';
import {useCanEditDetector} from 'sentry/views/detectors/utils/useCanEditDetector';

export function ProjectField() {
const {projects, fetching} = useProjects();
const {project, detectorType} = useDetectorFormContext();
const canEditDetector = useCanEditDetector({projectId: project.id, detectorType});
Comment on lines +11 to +12
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The ProjectField validation checks permissions against the initial project from context, not the currently selected project in the form, leading to incorrect permission errors.
Severity: MEDIUM

Suggested Fix

The validation logic should get the currently selected project ID from the form state using useFormField('projectId') instead of relying on the stale project from DetectorFormContext. This will ensure permissions are checked against the user's actual selection.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: static/app/views/detectors/components/forms/common/projectField.tsx#L11-L12

Potential issue: The `ProjectField` component's `validate` function relies on
`canEditDetector`, which is calculated using the `project` from
`useDetectorFormContext`. This context is initialized once and is not updated when the
user changes the project in the dropdown. As a result, the permission check is always
performed against the initial project, not the one currently selected in the form. This
can either incorrectly block a valid submission or incorrectly allow a submission for a
project the user lacks permission to edit on the client side.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Copy Markdown
Member Author

@malwilley malwilley Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this is just code that I moved over so it's not related to theses changes, but there does appear to be a bug here.

Created a linear issue: ISWF-2330


return (
<StyledProjectField
inline={false}
flexibleControlStateSize
stacked
projects={projects}
groupProjects={p => (p.isMember ? 'member' : 'all')}
groups={[
{key: 'member', label: t('My Projects')},
{key: 'all', label: t('All Projects')},
]}
name="projectId"
label={t('Project')}
placeholder={t('Project')}
aria-label={t('Select Project')}
disabled={fetching}
size="sm"
required
validate={() => {
if (!canEditDetector) {
return [
[
'projectId',
t('You do not have permission to create or edit monitors in this project'),
],
];
}
return [];
}}
/>
);
}

const StyledProjectField = styled(SentryProjectSelectorField)`
flex-grow: 1;
max-width: 260px;
padding: 0;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Container} from 'sentry/components/workflowEngine/ui/container';
import {Section} from 'sentry/components/workflowEngine/ui/section';
import {t} from 'sentry/locale';
import {ProjectField} from 'sentry/views/detectors/components/forms/common/projectField';

export function ProjectSection() {
return (
<Container>
<Section
title={t('Choose a Project')}
description={t('This is where issues will be created.')}
>
<ProjectField />
</Section>
</Container>
);
}
4 changes: 2 additions & 2 deletions static/app/views/detectors/components/forms/cron/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {CronDetector} from 'sentry/types/workflowEngine/detectors';
import {AutomateSection} from 'sentry/views/detectors/components/forms/automateSection';
import {AssignSection} from 'sentry/views/detectors/components/forms/common/assignSection';
import {DescribeSection} from 'sentry/views/detectors/components/forms/common/describeSection';
import {ProjectSection} from 'sentry/views/detectors/components/forms/common/projectSection';
import {CronDetectorFormDetectSection} from 'sentry/views/detectors/components/forms/cron/detect';
import {
CRON_DEFAULT_SCHEDULE_TYPE,
Expand Down Expand Up @@ -43,6 +44,7 @@ function CronDetectorForm({detector}: {detector?: CronDetector}) {
</Alert>
)}
<PreviewSection />
<ProjectSection />
<CronDetectorFormDetectSection />
<CronDetectorFormResolveSection />
<AssignSection />
Expand All @@ -69,7 +71,6 @@ export function NewCronDetectorForm() {
initialFormData={{
scheduleType: CRON_DEFAULT_SCHEDULE_TYPE,
}}
environment={false}
disabledCreate={
showingPlatformGuide
? t(
Expand All @@ -89,7 +90,6 @@ export function EditExistingCronDetectorForm({detector}: {detector: CronDetector
detector={detector}
formDataToEndpointPayload={cronFormDataToEndpointPayload}
savedDetectorToFormData={cronSavedDetectorToFormData}
environment={false}
>
<CronDetectorForm detector={detector} />
</EditDetectorLayout>
Expand Down
150 changes: 0 additions & 150 deletions static/app/views/detectors/components/forms/detectorBaseFields.tsx

This file was deleted.

Loading
Loading