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 (
{secretList.map(secret => {
return (
removeSecret({id: secret.id}) : undefined}
/>
);
})}
);
}
export function OrganizationFeatureFlagsChangeTracking() {
const organization = useOrganization();
const api = useApi();
const queryClient = useQueryClient();
const {
isPending,
isError,
data: secretList,
refetch: refetchSecretList,
} = useApiQuery(
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) => (
{t('Add New Provider')}
);
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 (
{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: (
),
}
)}
{t('Providers')}
{addNewProvider(hasAccess)}
{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.'
)}
) : 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 && (
)}
);
}
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)};
`;