Repository: getsentry/sentry
Pull request: feat(toolbar): add project settings page w/allowed_origins form
Depends on https://github.com/getsentry/sentry/pull/77674, closes https://github.com/getsentry/sentry/issues/77213

Notice the route on top + in the sidebar. Users need to manually click "Save Changes", which is only enabled if there were changes since the last save.
![Screenshot 2024-09-17 at 5 19 40 PM](https://github.com/user-attachments/assets/043667cc-33d5-463f-8c50-38bca6842264)

TODO: format and validate before submission. Can use regex or whatever we use for similar forms, [inbound filters](https://sentry.sentry.io/settings/projects/javascript/filters/data-filters/#filters-react-hydration-errors_help) is one
Questions:
- yes or no to trailing `/`'s?
- require protocol or fill in one by default? E.g. `https://`

https://github.com/getsentry/sentry/pull/77674 sets `["*"]` (allow all) as the default. Whatever it is, we should probably show a default value in the form to be consistent.

Problem statement:
[DevToolbar] Create a settings page for the Toolbar
The page should be behind a feature flag!

The settings should be per-project.

To start we should let people have a text area to save a list of domains (new-line separated) that are allowed to host the toolbar. For sentry itself I'd expect the list to be: 
```
*.sentry.io
dev.getsentry.net:8000
*.dev.getsentry.net:7999
*.sentry.dev
```
which represents:
- prod
- local devapp
- local dev-ui
- previews on CI

It'll be important for people to include the port as well, so maybe we'll make a note of that. protocol is not important.

Hints from issue discussion:

Commit message: feat(toolbar): add project settings page w/allowed_origins form

Patch to review:
diff --git a/static/app/routes.tsx b/static/app/routes.tsx
index 992570b648c935..144e17d54e732a 100644
--- a/static/app/routes.tsx
+++ b/static/app/routes.tsx
@@ -557,6 +557,11 @@ function buildRoutes() {
         name={t('Replays')}
         component={make(() => import('sentry/views/settings/project/projectReplays'))}
       />
+      <Route
+        path="toolbar/"
+        name={t('Developer Toolbar')}
+        component={make(() => import('sentry/views/settings/project/toolbar'))}
+      />
       <Route path="source-maps/" name={t('Source Maps')}>
         <IndexRoute
           component={make(() => import('sentry/views/settings/projectSourceMaps'))}
diff --git a/static/app/views/settings/project/navigationConfiguration.tsx b/static/app/views/settings/project/navigationConfiguration.tsx
index c3dbd1e025c754..e6652878167d83 100644
--- a/static/app/views/settings/project/navigationConfiguration.tsx
+++ b/static/app/views/settings/project/navigationConfiguration.tsx
@@ -64,6 +64,11 @@ export default function getConfiguration({
           title: t('User Feedback'),
           show: () => !isSelfHostedErrorsOnly,
         },
+        {
+          path: `${pathPrefix}/toolbar/`,
+          title: t('Developer Toolbar'),
+          show: () => true || !!organization?.features?.includes('dev-toolbar-ui'),
+        },
       ],
     },
     {
diff --git a/static/app/views/settings/project/toolbar.tsx b/static/app/views/settings/project/toolbar.tsx
new file mode 100644
index 00000000000000..b7050476004005
--- /dev/null
+++ b/static/app/views/settings/project/toolbar.tsx
@@ -0,0 +1,71 @@
+import Access from 'sentry/components/acl/access';
+import Form from 'sentry/components/forms/form';
+import JsonForm from 'sentry/components/forms/jsonForm';
+import type {JsonFormObject} from 'sentry/components/forms/types';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import {t} from 'sentry/locale';
+import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
+import type {Organization} from 'sentry/types/organization';
+import type {Project} from 'sentry/types/project';
+import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
+import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
+
+type RouteParams = {
+  projectId: string;
+};
+type Props = RouteComponentProps<RouteParams, {}> & {
+  organization: Organization;
+  project: Project;
+};
+
+function ProjectToolbarSettings({organization, project, params: {projectId}}: Props) {
+  const formGroups: JsonFormObject[] = [
+    {
+      title: 'Settings',
+      fields: [
+        {
+          name: 'sentry:toolbar_allowed_origins',
+          type: 'textarea',
+          rows: 3,
+          autosize: true,
+
+          // additional data/props that is related to rendering of form field rather than data
+          label: t('Allowed Origins'),
+          help: t(
+            'Comma-separated list of URLs where the dev toolbar can be installed and access your data. Wildcards (*) are supported'
+          ),
+          getData: data => ({options: data}),
+          visible({features}) {
+            return features.has('dev-toolbar-ui');
+          },
+        },
+      ],
+    },
+  ];
+
+  return (
+    <SentryDocumentTitle title={t('Toolbar Settings')} projectSlug={project.slug}>
+      <SettingsPageHeader title={t('Developer Toolbar')} />
+      <PermissionAlert project={project} />
+
+      <Form
+        apiMethod="PUT"
+        apiEndpoint={`/projects/${organization.slug}/${projectId}/`}
+        initialData={project.options}
+        requireChanges
+      >
+        <Access access={['project:write']} project={project}>
+          {({hasAccess}) => (
+            <JsonForm
+              disabled={!hasAccess}
+              features={new Set(organization.features)}
+              forms={formGroups}
+            />
+          )}
+        </Access>
+      </Form>
+    </SentryDocumentTitle>
+  );
+}
+
+export default ProjectToolbarSettings;


Code review comments:
diff_hunk: @@ -64,6 +64,11 @@ export default function getConfiguration({
           title: t('User Feedback'),
           show: () => !isSelfHostedErrorsOnly,
         },
+        {
+          path: `${pathPrefix}/toolbar/`,
+          title: t('Developer Toolbar'),
+          show: () => true || !!organization?.features?.includes('dev-toolbar-ui'),
path: static/app/views/settings/project/navigationConfiguration.tsx
text: @user1:
TODO: don't ship the `true ||` part when the time comes.

@user1:
```suggestion
          show: () => organization?.features?.includes('dev-toolbar-ui'),
```

@author:
Good catch 😅