Browse Source

ref(flags): split FF settings into eval/change tracking (#86070)

Figma: [settings page
v2](https://www.figma.com/design/xZ22Ox8OqhKVkJB2TGi1Vo/Specs%3A-Settings-Page-v2?node-id=2033-3118&p=f&m=dev)
Closes https://github.com/getsentry/team-replay/issues/544


https://github.com/user-attachments/assets/ccfc4b18-0f9e-45ac-8b09-0be2ebb62f4f

Updated flyout: 

![image](https://github.com/user-attachments/assets/815e80af-42c4-4b48-8bd0-d0c1c2f783aa)
New Change Tracking link takes you to change tracking settings.
Andrew Liu 1 week ago
parent
commit
0dc6dfd71a

+ 33 - 13
static/app/components/events/featureFlags/featureFlagOnboardingLayout.tsx

@@ -1,10 +1,10 @@
 import {useMemo, useState} from 'react';
 import {useMemo, useState} from 'react';
 import styled from '@emotion/styled';
 import styled from '@emotion/styled';
 
 
-import {Button} from 'sentry/components/button';
+import {Button, LinkButton} from 'sentry/components/button';
 import {Flex} from 'sentry/components/container/flex';
 import {Flex} from 'sentry/components/container/flex';
 import {Alert} from 'sentry/components/core/alert';
 import {Alert} from 'sentry/components/core/alert';
-import OnboardingIntegrationSection from 'sentry/components/events/featureFlags/onboardingIntegrationSection';
+import OnboardingAdditionalFeatures from 'sentry/components/events/featureFlags/onboardingAdditionalInfo';
 import {AuthTokenGeneratorProvider} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator';
 import {AuthTokenGeneratorProvider} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator';
 import type {OnboardingLayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/onboardingLayout';
 import type {OnboardingLayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/onboardingLayout';
 import {Step} from 'sentry/components/onboarding/gettingStartedDoc/step';
 import {Step} from 'sentry/components/onboarding/gettingStartedDoc/step';
@@ -20,8 +20,7 @@ import useOrganization from 'sentry/utils/useOrganization';
 
 
 interface FeatureFlagOnboardingLayoutProps extends OnboardingLayoutProps {
 interface FeatureFlagOnboardingLayoutProps extends OnboardingLayoutProps {
   integration?: string;
   integration?: string;
-  provider?: string;
-  skipConfig?: boolean;
+  skipEvalTracking?: boolean;
 }
 }
 
 
 export function FeatureFlagOnboardingLayout({
 export function FeatureFlagOnboardingLayout({
@@ -33,8 +32,7 @@ export function FeatureFlagOnboardingLayout({
   projectKeyId,
   projectKeyId,
   configType = 'onboarding',
   configType = 'onboarding',
   integration = '',
   integration = '',
-  provider = '',
-  skipConfig,
+  skipEvalTracking,
 }: FeatureFlagOnboardingLayoutProps) {
 }: FeatureFlagOnboardingLayoutProps) {
   const api = useApi();
   const api = useApi();
   const organization = useOrganization();
   const organization = useOrganization();
@@ -42,7 +40,7 @@ export function FeatureFlagOnboardingLayout({
     useSourcePackageRegistries(organization);
     useSourcePackageRegistries(organization);
   const selectedOptions = useUrlPlatformOptions(docsConfig.platformOptions);
   const selectedOptions = useUrlPlatformOptions(docsConfig.platformOptions);
   const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore);
   const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore);
-  const [skipSteps, setSkipSteps] = useState(skipConfig);
+  const [hideSteps, setHideSteps] = useState(skipEvalTracking);
 
 
   const {steps} = useMemo(() => {
   const {steps} = useMemo(() => {
     const doc = docsConfig[configType] ?? docsConfig.onboarding;
     const doc = docsConfig[configType] ?? docsConfig.onboarding;
@@ -95,28 +93,32 @@ export function FeatureFlagOnboardingLayout({
   return (
   return (
     <AuthTokenGeneratorProvider projectSlug={projectSlug}>
     <AuthTokenGeneratorProvider projectSlug={projectSlug}>
       <Wrapper>
       <Wrapper>
-        {!skipConfig ? null : (
+        {skipEvalTracking ? (
           <Alert.Container>
           <Alert.Container>
             <Alert type="info" showIcon>
             <Alert type="info" showIcon>
               <Flex gap={space(3)}>
               <Flex gap={space(3)}>
                 {t(
                 {t(
                   'Feature flag integration detected. Please follow the remaining steps.'
                   'Feature flag integration detected. Please follow the remaining steps.'
                 )}
                 )}
-                <Button onClick={() => setSkipSteps(!skipSteps)}>
-                  {skipSteps ? t('Show Full Guide') : t('Hide Full Guide')}
+                <Button onClick={() => setHideSteps(!hideSteps)}>
+                  {hideSteps ? t('Show Full Guide') : t('Hide Full Guide')}
                 </Button>
                 </Button>
               </Flex>
               </Flex>
             </Alert>
             </Alert>
           </Alert.Container>
           </Alert.Container>
-        )}
-        {!skipSteps && (
+        ) : null}
+        {hideSteps ? null : (
           <Steps>
           <Steps>
             {steps.map(step => (
             {steps.map(step => (
               <Step key={step.title ?? step.type} {...step} />
               <Step key={step.title ?? step.type} {...step} />
             ))}
             ))}
+            <StyledLinkButton to="/issues/" priority="primary">
+              {t('Take me to Issues')}
+            </StyledLinkButton>
           </Steps>
           </Steps>
         )}
         )}
-        <OnboardingIntegrationSection provider={provider} integration={integration} />
+        <Divider />
+        <OnboardingAdditionalFeatures organization={organization} />
       </Wrapper>
       </Wrapper>
     </AuthTokenGeneratorProvider>
     </AuthTokenGeneratorProvider>
   );
   );
@@ -128,6 +130,10 @@ const Steps = styled('div')`
   gap: 1.5rem;
   gap: 1.5rem;
 `;
 `;
 
 
+const StyledLinkButton = styled(LinkButton)`
+  align-self: flex-start;
+`;
+
 const Wrapper = styled('div')`
 const Wrapper = styled('div')`
   h4 {
   h4 {
     margin-bottom: 0.5em;
     margin-bottom: 0.5em;
@@ -141,3 +147,17 @@ const Wrapper = styled('div')`
     }
     }
   }
   }
 `;
 `;
+
+const Divider = styled('div')`
+  position: relative;
+  margin-top: ${space(3)};
+  &:before {
+    display: block;
+    position: absolute;
+    content: '';
+    height: 1px;
+    left: 0;
+    right: 0;
+    background: ${p => p.theme.border};
+  }
+`;

+ 16 - 31
static/app/components/events/featureFlags/featureFlagOnboardingSidebar.tsx

@@ -34,6 +34,7 @@ import type {SelectValue} from 'sentry/types/core';
 import type {Project} from 'sentry/types/project';
 import type {Project} from 'sentry/types/project';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
 import useUrlParams from 'sentry/utils/useUrlParams';
 import useUrlParams from 'sentry/utils/useUrlParams';
+import TextBlock from 'sentry/views/settings/components/text/textBlock';
 
 
 export function useFeatureFlagOnboardingDrawer() {
 export function useFeatureFlagOnboardingDrawer() {
   const organization = useOrganization();
   const organization = useOrganization();
@@ -88,8 +89,6 @@ function LegacyFeatureFlagOnboardingSidebar(props: CommonSidebarProps) {
 
 
 function SidebarContent() {
 function SidebarContent() {
   const {
   const {
-    hasDocs,
-    projects,
     allProjects,
     allProjects,
     currentProject,
     currentProject,
     setCurrentProject,
     setCurrentProject,
@@ -139,12 +138,6 @@ function SidebarContent() {
     ];
     ];
   }, [supportedProjects, unsupportedProjects]);
   }, [supportedProjects, unsupportedProjects]);
 
 
-  const selectedProject = currentProject ?? projects[0] ?? allProjects[0];
-
-  if (!selectedProject) {
-    return <LoadingIndicator />;
-  }
-
   return (
   return (
     <Fragment>
     <Fragment>
       <TopRightBackgroundImage src={HighlightTopRightPattern} />
       <TopRightBackgroundImage src={HighlightTopRightPattern} />
@@ -183,19 +176,19 @@ function SidebarContent() {
             />
             />
           </div>
           </div>
         </HeaderActions>
         </HeaderActions>
-        <OnboardingContent currentProject={selectedProject} hasDocs={hasDocs} />
+        {currentProject ? (
+          <OnboardingContent currentProject={currentProject} />
+        ) : (
+          <TextBlock>
+            {t('Select a project from the drop-down to view set up instructions.')}
+          </TextBlock>
+        )}
       </TaskList>
       </TaskList>
     </Fragment>
     </Fragment>
   );
   );
 }
 }
 
 
-function OnboardingContent({
-  currentProject,
-  hasDocs,
-}: {
-  currentProject: Project;
-  hasDocs: boolean;
-}) {
+function OnboardingContent({currentProject}: {currentProject: Project}) {
   const organization = useOrganization();
   const organization = useOrganization();
 
 
   // useMemo is needed to remember the original hash
   // useMemo is needed to remember the original hash
@@ -203,7 +196,7 @@ function OnboardingContent({
   const ORIGINAL_HASH = useMemo(() => {
   const ORIGINAL_HASH = useMemo(() => {
     return window.location.hash;
     return window.location.hash;
   }, []);
   }, []);
-  const skipConfig = ORIGINAL_HASH === FLAG_HASH_SKIP_CONFIG;
+  const skipEvalTracking = ORIGINAL_HASH === FLAG_HASH_SKIP_CONFIG;
 
 
   // First dropdown: OpenFeature providers
   // First dropdown: OpenFeature providers
   const openFeatureProviderOptions = Object.values(WebhookProviderEnum).map(provider => {
   const openFeatureProviderOptions = Object.values(WebhookProviderEnum).map(provider => {
@@ -329,7 +322,10 @@ function OnboardingContent({
         {t(
         {t(
           'To see which feature flags changed over time, visit the settings page to set up a webhook for your Feature Flag provider.'
           'To see which feature flags changed over time, visit the settings page to set up a webhook for your Feature Flag provider.'
         )}
         )}
-        <LinkButton size="sm" href={`/settings/${organization.slug}/feature-flags/`}>
+        <LinkButton
+          size="sm"
+          href={`/settings/${organization.slug}/feature-flags/change-tracking/`}
+        >
           {t('Go to Feature Flag Settings')}
           {t('Go to Feature Flag Settings')}
         </LinkButton>
         </LinkButton>
       </StyledDefaultContent>
       </StyledDefaultContent>
@@ -369,14 +365,7 @@ function OnboardingContent({
   }
   }
 
 
   // Platform is not supported, no platform, docs import failed, no DSN, or the platform doesn't have onboarding yet
   // Platform is not supported, no platform, docs import failed, no DSN, or the platform doesn't have onboarding yet
-  if (
-    doesNotSupportFeatureFlags ||
-    !currentPlatform ||
-    !docs ||
-    !dsn ||
-    !hasDocs ||
-    !projectKeyId
-  ) {
+  if (doesNotSupportFeatureFlags || !currentPlatform || !docs || !dsn || !projectKeyId) {
     return defaultMessage;
     return defaultMessage;
   }
   }
 
 
@@ -384,7 +373,7 @@ function OnboardingContent({
     <Fragment>
     <Fragment>
       {radioButtons}
       {radioButtons}
       <FeatureFlagOnboardingLayout
       <FeatureFlagOnboardingLayout
-        skipConfig={skipConfig}
+        skipEvalTracking={skipEvalTracking}
         docsConfig={docs}
         docsConfig={docs}
         dsn={dsn}
         dsn={dsn}
         projectKeyId={projectKeyId}
         projectKeyId={projectKeyId}
@@ -396,10 +385,6 @@ function OnboardingContent({
           // either OpenFeature or the SDK selected from the second dropdown
           // either OpenFeature or the SDK selected from the second dropdown
           setupMode() === 'openFeature' ? SdkProviderEnum.OPENFEATURE : sdkProvider.value
           setupMode() === 'openFeature' ? SdkProviderEnum.OPENFEATURE : sdkProvider.value
         }
         }
-        provider={
-          // dropdown value (from either dropdown)
-          setupMode() === 'openFeature' ? openFeatureProvider.value : sdkProvider.value
-        }
         configType="featureFlagOnboarding"
         configType="featureFlagOnboarding"
       />
       />
     </Fragment>
     </Fragment>

+ 25 - 0
static/app/components/events/featureFlags/onboardingAdditionalInfo.tsx

@@ -0,0 +1,25 @@
+import {Fragment} from 'react';
+
+import Link from 'sentry/components/links/link';
+import {t, tct} from 'sentry/locale';
+import type {Organization} from 'sentry/types/organization';
+
+export default function OnboardingAdditionalFeatures({
+  organization,
+}: {
+  organization: Organization;
+}) {
+  return (
+    <Fragment>
+      <h4 style={{marginTop: '40px'}}>{t('Additional Features')}</h4>
+      {tct(
+        '[link:Change Tracking]: Configure Sentry to listen for additions, removals, and modifications to your feature flags.',
+        {
+          link: (
+            <Link to={`/settings/${organization.slug}/feature-flags/change-tracking/`} />
+          ),
+        }
+      )}
+    </Fragment>
+  );
+}

+ 2 - 2
static/app/components/events/featureFlags/onboardingIntegrationSection.tsx

@@ -26,11 +26,11 @@ import {useMutation, useQueryClient} from 'sentry/utils/queryClient';
 import type RequestError from 'sentry/utils/requestError/requestError';
 import type RequestError from 'sentry/utils/requestError/requestError';
 import useApi from 'sentry/utils/useApi';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
-import {makeFetchSecretQueryKey} from 'sentry/views/settings/featureFlags';
+import {makeFetchSecretQueryKey} from 'sentry/views/settings/featureFlags/changeTracking';
 import type {
 import type {
   CreateSecretQueryVariables,
   CreateSecretQueryVariables,
   CreateSecretResponse,
   CreateSecretResponse,
-} from 'sentry/views/settings/featureFlags/newProviderForm';
+} from 'sentry/views/settings/featureFlags/changeTracking/newProviderForm';
 
 
 export default function OnboardingIntegrationSection({
 export default function OnboardingIntegrationSection({
   provider,
   provider,

+ 1 - 1
static/app/components/events/featureFlags/useFeatureFlagOnboarding.tsx

@@ -31,7 +31,7 @@ export function useFeatureFlagOnboarding() {
   }, []);
   }, []);
 
 
   // if we detect that event.contexts.flags is set, use this hook instead
   // if we detect that event.contexts.flags is set, use this hook instead
-  // to skip the configure step
+  // to hide the eval tracking SDK configuration.
   const activateSidebarSkipConfigure = useCallback(
   const activateSidebarSkipConfigure = useCallback(
     (event: React.MouseEvent, projectId: string) => {
     (event: React.MouseEvent, projectId: string) => {
       event.preventDefault();
       event.preventDefault();

+ 17 - 10
static/app/routes.tsx

@@ -1010,16 +1010,23 @@ function buildRoutes() {
         <IndexRoute
         <IndexRoute
           component={make(() => import('sentry/views/settings/featureFlags'))}
           component={make(() => import('sentry/views/settings/featureFlags'))}
         />
         />
-        <Route
-          path="new-provider/"
-          name={t('Add New Provider')}
-          component={make(
-            () =>
-              import(
-                'sentry/views/settings/featureFlags/organizationFeatureFlagsNewSecret'
-              )
-          )}
-        />
+        <Route path="change-tracking/" name={t('Change Tracking')}>
+          <IndexRoute
+            component={make(
+              () => import('sentry/views/settings/featureFlags/changeTracking')
+            )}
+          />
+          <Route
+            path="new-provider/"
+            name={t('Add New Provider')}
+            component={make(
+              () =>
+                import(
+                  'sentry/views/settings/featureFlags/changeTracking/organizationFeatureFlagsNewSecret'
+                )
+            )}
+          />
+        </Route>
       </Route>
       </Route>
       <Route path="stats/" name={t('Stats')}>
       <Route path="stats/" name={t('Stats')}>
         {statsChildRoutes}
         {statsChildRoutes}

+ 7 - 7
static/app/views/settings/featureFlags/index.spec.tsx → static/app/views/settings/featureFlags/changeTracking/index.spec.tsx

@@ -13,9 +13,9 @@ import {
 import * as indicators from 'sentry/actionCreators/indicator';
 import * as indicators from 'sentry/actionCreators/indicator';
 import OrganizationsStore from 'sentry/stores/organizationsStore';
 import OrganizationsStore from 'sentry/stores/organizationsStore';
 import {
 import {
-  OrganizationFeatureFlagsIndex,
+  OrganizationFeatureFlagsChangeTracking,
   type Secret,
   type Secret,
-} from 'sentry/views/settings/featureFlags';
+} from 'sentry/views/settings/featureFlags/changeTracking';
 
 
 describe('OrganizationFeatureFlagsIndex', function () {
 describe('OrganizationFeatureFlagsIndex', function () {
   const SECRETS_ENDPOINT = '/organizations/org-slug/flags/signing-secrets/';
   const SECRETS_ENDPOINT = '/organizations/org-slug/flags/signing-secrets/';
@@ -51,7 +51,7 @@ describe('OrganizationFeatureFlagsIndex', function () {
       body: {data: secrets},
       body: {data: secrets},
     });
     });
 
 
-    render(<OrganizationFeatureFlagsIndex />);
+    render(<OrganizationFeatureFlagsChangeTracking />);
 
 
     const secretsTable = within(screen.getByTestId('secrets-table'));
     const secretsTable = within(screen.getByTestId('secrets-table'));
 
 
@@ -76,7 +76,7 @@ describe('OrganizationFeatureFlagsIndex', function () {
       statusCode: 400,
       statusCode: 400,
     });
     });
 
 
-    render(<OrganizationFeatureFlagsIndex />);
+    render(<OrganizationFeatureFlagsChangeTracking />);
 
 
     const secretsTable = within(screen.getByTestId('secrets-table'));
     const secretsTable = within(screen.getByTestId('secrets-table'));
 
 
@@ -98,7 +98,7 @@ describe('OrganizationFeatureFlagsIndex', function () {
       body: {data: secrets},
       body: {data: secrets},
     });
     });
 
 
-    render(<OrganizationFeatureFlagsIndex />);
+    render(<OrganizationFeatureFlagsChangeTracking />);
 
 
     const secretsTable = within(screen.getByTestId('secrets-table'));
     const secretsTable = within(screen.getByTestId('secrets-table'));
 
 
@@ -127,7 +127,7 @@ describe('OrganizationFeatureFlagsIndex', function () {
         method: 'DELETE',
         method: 'DELETE',
       });
       });
 
 
-      render(<OrganizationFeatureFlagsIndex />);
+      render(<OrganizationFeatureFlagsChangeTracking />);
       renderGlobalModal();
       renderGlobalModal();
 
 
       const secretsTable = within(screen.getByTestId('secrets-table'));
       const secretsTable = within(screen.getByTestId('secrets-table'));
@@ -174,7 +174,7 @@ describe('OrganizationFeatureFlagsIndex', function () {
         body: {data: secrets},
         body: {data: secrets},
       });
       });
 
 
-      render(<OrganizationFeatureFlagsIndex />, {organization: org});
+      render(<OrganizationFeatureFlagsChangeTracking />, {organization: org});
 
 
       const secretsTable = within(screen.getByTestId('secrets-table'));
       const secretsTable = within(screen.getByTestId('secrets-table'));
 
 

+ 215 - 0
static/app/views/settings/featureFlags/changeTracking/index.tsx

@@ -0,0 +1,215 @@
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
+import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
+import {hasEveryAccess} from 'sentry/components/acl/access';
+import {LinkButton} from 'sentry/components/button';
+import {Flex} from 'sentry/components/container/flex';
+import ExternalLink from 'sentry/components/links/externalLink';
+import LoadingError from 'sentry/components/loadingError';
+import {PanelTable} from 'sentry/components/panels/panelTable';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import {Tooltip} from 'sentry/components/tooltip';
+import {t, tct} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
+import {
+  setApiQueryData,
+  useApiQuery,
+  useMutation,
+  useQueryClient,
+} from 'sentry/utils/queryClient';
+import type RequestError from 'sentry/utils/requestError/requestError';
+import useApi from 'sentry/utils/useApi';
+import useOrganization from 'sentry/utils/useOrganization';
+import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
+import TextBlock from 'sentry/views/settings/components/text/textBlock';
+import {OrganizationFeatureFlagsAuditLogTable} from 'sentry/views/settings/featureFlags/changeTracking/organizationFeatureFlagsAuditLogTable';
+import {OrganizationFeatureFlagsProviderRow} from 'sentry/views/settings/featureFlags/changeTracking/organizationFeatureFlagsProviderRow';
+
+export type Secret = {
+  createdAt: string;
+  createdBy: number;
+  id: number;
+  provider: string;
+  secret: string;
+};
+
+type FetchSecretResponse = {data: Secret[]};
+
+type FetchSecretParameters = {
+  orgSlug: string;
+};
+
+type RemoveSecretQueryVariables = {
+  id: number;
+};
+
+export const makeFetchSecretQueryKey = ({orgSlug}: FetchSecretParameters) =>
+  [`/organizations/${orgSlug}/flags/signing-secrets/`] as const;
+
+function SecretList({
+  secretList,
+  isRemoving,
+  removeSecret,
+}: {
+  isRemoving: boolean;
+  secretList: Secret[];
+  removeSecret?: (data: {id: number}) => void;
+}) {
+  return (
+    <Fragment>
+      {secretList.map(secret => {
+        return (
+          <OrganizationFeatureFlagsProviderRow
+            key={secret.id}
+            secret={secret}
+            isRemoving={isRemoving}
+            removeSecret={removeSecret ? () => removeSecret({id: secret.id}) : undefined}
+          />
+        );
+      })}
+    </Fragment>
+  );
+}
+
+export function OrganizationFeatureFlagsChangeTracking() {
+  const organization = useOrganization();
+  const api = useApi();
+  const queryClient = useQueryClient();
+
+  const {
+    isPending,
+    isError,
+    data: secretList,
+    refetch: refetchSecretList,
+  } = useApiQuery<FetchSecretResponse>(
+    makeFetchSecretQueryKey({orgSlug: organization.slug}),
+    {
+      staleTime: Infinity,
+    }
+  );
+
+  const {mutate: handleRemoveSecret, isPending: isRemoving} = useMutation<
+    unknown,
+    RequestError,
+    RemoveSecretQueryVariables
+  >({
+    mutationFn: ({id}) =>
+      api.requestPromise(
+        `/organizations/${organization.slug}/flags/signing-secrets/${id}/`,
+        {
+          method: 'DELETE',
+        }
+      ),
+
+    onSuccess: (_data, {id}) => {
+      addSuccessMessage(
+        t('Removed the provider and signing secret for the organization.')
+      );
+
+      setApiQueryData(
+        queryClient,
+        makeFetchSecretQueryKey({orgSlug: organization.slug}),
+        (oldData: FetchSecretResponse) => {
+          return {data: oldData.data.filter(oldSecret => oldSecret.id !== id)};
+        }
+      );
+    },
+    onError: error => {
+      const message = t('Failed to remove the provider or signing secret.');
+      handleXhrErrorResponse(message, error);
+      addErrorMessage(message);
+    },
+  });
+
+  const addNewProvider = (hasAccess: any) => (
+    <Tooltip
+      title={t('You must be an organization member to add a provider.')}
+      disabled={hasAccess}
+    >
+      <LinkButton
+        priority="primary"
+        size="sm"
+        to={`/settings/${organization.slug}/feature-flags/change-tracking/new-provider/`}
+        data-test-id="create-new-provider"
+        disabled={!hasAccess}
+      >
+        {t('Add New Provider')}
+      </LinkButton>
+    </Tooltip>
+  );
+
+  const canRead = hasEveryAccess(['org:read'], {organization});
+  const canWrite = hasEveryAccess(['org:write'], {organization});
+  const canAdmin = hasEveryAccess(['org:admin'], {organization});
+  const hasAccess = canRead || canWrite || canAdmin;
+  const hasDeleteAccess = canWrite || canAdmin;
+
+  return (
+    <Fragment>
+      <SentryDocumentTitle title={t('Change Tracking')} orgSlug={organization.slug} />
+      <SettingsPageHeader title={t('Change Tracking')} />
+      <TextBlock>
+        {tct(
+          'Integrating Sentry with your feature flag provider enables Sentry to correlate feature flag changes with new error events and mark certain changes as suspicious. Learn more about how to interact with feature flag insights within the Sentry UI by reading the [link:documentation].',
+          {
+            link: (
+              <ExternalLink href="https://docs.sentry.io/product/explore/feature-flags/#change-tracking" />
+            ),
+          }
+        )}
+      </TextBlock>
+
+      <Flex justify="space-between">
+        <h5>{t('Providers')}</h5>
+        {addNewProvider(hasAccess)}
+      </Flex>
+
+      <TextBlock>
+        {t(
+          'Look below for a list of the webhooks you have set up with external providers. Note that each provider can only have one associated signing secret.'
+        )}
+      </TextBlock>
+      <ResponsivePanelTable
+        isLoading={isPending || isError}
+        isEmpty={!isPending && !secretList?.data?.length}
+        loader={
+          isError ? (
+            <LoadingError
+              message={t('Failed to load secrets and providers for the organization.')}
+              onRetry={refetchSecretList}
+            />
+          ) : undefined
+        }
+        emptyMessage={t("You haven't linked any providers yet.")}
+        headers={[t('Provider'), t('Created'), t('Created by'), '']}
+        data-test-id="secrets-table"
+      >
+        {!isError && !isPending && !!secretList?.data?.length && (
+          <SecretList
+            secretList={secretList.data}
+            isRemoving={isRemoving}
+            removeSecret={hasDeleteAccess ? handleRemoveSecret : undefined}
+          />
+        )}
+      </ResponsivePanelTable>
+
+      <OrganizationFeatureFlagsAuditLogTable />
+    </Fragment>
+  );
+}
+
+export default OrganizationFeatureFlagsChangeTracking;
+
+const ResponsivePanelTable = styled(PanelTable)`
+  @media (max-width: ${p => p.theme.breakpoints.small}) {
+    grid-template-columns: 1fr 1fr;
+
+    > *:nth-child(4n + 2),
+    > *:nth-child(4n + 3) {
+      display: none;
+    }
+  }
+  margin-bottom: ${space(3)};
+`;

+ 1 - 1
static/app/views/settings/featureFlags/newProviderForm.spec.tsx → static/app/views/settings/featureFlags/changeTracking/newProviderForm.spec.tsx

@@ -1,6 +1,6 @@
 import {render} from 'sentry-test/reactTestingLibrary';
 import {render} from 'sentry-test/reactTestingLibrary';
 
 
-import NewProviderForm from 'sentry/views/settings/featureFlags/newProviderForm';
+import NewProviderForm from 'sentry/views/settings/featureFlags/changeTracking/newProviderForm';
 
 
 describe('NewProviderForm', () => {
 describe('NewProviderForm', () => {
   it('renders', () => {
   it('renders', () => {

+ 4 - 2
static/app/views/settings/featureFlags/newProviderForm.tsx → static/app/views/settings/featureFlags/changeTracking/newProviderForm.tsx

@@ -26,7 +26,7 @@ import normalizeUrl from 'sentry/utils/url/normalizeUrl';
 import useApi from 'sentry/utils/useApi';
 import useApi from 'sentry/utils/useApi';
 import {useNavigate} from 'sentry/utils/useNavigate';
 import {useNavigate} from 'sentry/utils/useNavigate';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
-import {makeFetchSecretQueryKey} from 'sentry/views/settings/featureFlags';
+import {makeFetchSecretQueryKey} from 'sentry/views/settings/featureFlags/changeTracking';
 
 
 export type CreateSecretQueryVariables = {
 export type CreateSecretQueryVariables = {
   provider: string;
   provider: string;
@@ -54,7 +54,9 @@ export default function NewProviderForm({
   const [selectedProvider, setSelectedProvider] = useState('<provider_name>');
   const [selectedProvider, setSelectedProvider] = useState('<provider_name>');
 
 
   const handleGoBack = useCallback(() => {
   const handleGoBack = useCallback(() => {
-    navigate(normalizeUrl(`/settings/${organization.slug}/feature-flags/`));
+    navigate(
+      normalizeUrl(`/settings/${organization.slug}/feature-flags/change-tracking/`)
+    );
   }, [organization.slug, navigate]);
   }, [organization.slug, navigate]);
 
 
   const {mutate: submitSecret, isPending} = useMutation<
   const {mutate: submitSecret, isPending} = useMutation<

Some files were not shown because too many files changed in this diff