organizationFeatureFlagsAuditLogTable.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import {Fragment, useMemo, useState} from 'react';
  2. import {Tag} from 'sentry/components/core/badge/tag';
  3. import GridEditable, {type GridColumnOrder} from 'sentry/components/gridEditable';
  4. import Pagination from 'sentry/components/pagination';
  5. import useQueryBasedColumnResize from 'sentry/components/replays/useQueryBasedColumnResize';
  6. import {t} from 'sentry/locale';
  7. import {trackAnalytics} from 'sentry/utils/analytics';
  8. import {FIELD_FORMATTERS} from 'sentry/utils/discover/fieldRenderers';
  9. import {decodeScalar} from 'sentry/utils/queryString';
  10. import useLocationQuery from 'sentry/utils/url/useLocationQuery';
  11. import {useLocation} from 'sentry/utils/useLocation';
  12. import {useNavigate} from 'sentry/utils/useNavigate';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import type {RawFlag} from 'sentry/views/issueDetails/streamline/featureFlagUtils';
  15. import {useOrganizationFlagLog} from 'sentry/views/issueDetails/streamline/hooks/useOrganizationFlagLog';
  16. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  17. type ColumnKey = 'provider' | 'flag' | 'action' | 'createdAt';
  18. const BASE_COLUMNS: Array<GridColumnOrder<ColumnKey>> = [
  19. {key: 'provider', name: t('Provider')},
  20. {key: 'flag', name: t('Feature Flag'), width: 600},
  21. {key: 'action', name: t('Action')},
  22. {key: 'createdAt', name: t('Created')},
  23. ];
  24. export function OrganizationFeatureFlagsAuditLogTable({
  25. pageSize = 15,
  26. }: {
  27. pageSize?: number;
  28. }) {
  29. const organization = useOrganization();
  30. const navigate = useNavigate();
  31. const location = useLocation();
  32. const locationQuery = useLocationQuery({
  33. fields: {
  34. cursor: decodeScalar,
  35. end: decodeScalar,
  36. flag: decodeScalar,
  37. sort: (value: any) => decodeScalar(value, '-created_at'),
  38. start: decodeScalar,
  39. statsPeriod: decodeScalar,
  40. utc: decodeScalar,
  41. },
  42. });
  43. const query = useMemo(() => {
  44. const filteredFields = Object.fromEntries(
  45. Object.entries(locationQuery).filter(([_key, val]) => val !== '')
  46. );
  47. return {
  48. ...filteredFields,
  49. per_page: pageSize,
  50. queryReferrer: 'featureFlagsSettings',
  51. };
  52. }, [locationQuery, pageSize]);
  53. const {
  54. data: responseData,
  55. isPending,
  56. error,
  57. getResponseHeader,
  58. } = useOrganizationFlagLog({
  59. organization,
  60. query,
  61. });
  62. const pageLinks = getResponseHeader?.('Link') ?? null;
  63. const [activeRowKey, setActiveRowKey] = useState<number | undefined>(undefined);
  64. const renderBodyCell = (
  65. column: GridColumnOrder<ColumnKey>,
  66. dataRow: RawFlag,
  67. _rowIndex: number,
  68. _columnIndex: number
  69. ) => {
  70. switch (column.key) {
  71. case 'flag':
  72. return <code>{dataRow.flag}</code>;
  73. case 'provider':
  74. return dataRow.provider || t('unknown');
  75. case 'createdAt':
  76. return FIELD_FORMATTERS.date.renderFunc('createdAt', dataRow);
  77. case 'action': {
  78. const type =
  79. dataRow.action === 'created'
  80. ? 'info'
  81. : dataRow.action === 'deleted'
  82. ? 'error'
  83. : undefined;
  84. const capitalized =
  85. dataRow.action.charAt(0).toUpperCase() + dataRow.action.slice(1);
  86. return (
  87. <div style={{alignSelf: 'flex-start'}}>
  88. <Tag type={type}>{capitalized}</Tag>
  89. </div>
  90. );
  91. }
  92. default:
  93. return dataRow[column.key!];
  94. }
  95. };
  96. const {columns, handleResizeColumn} = useQueryBasedColumnResize({
  97. columns: BASE_COLUMNS,
  98. location,
  99. });
  100. return (
  101. <Fragment>
  102. <h5>{t('Audit Logs')}</h5>
  103. <TextBlock>
  104. {t(
  105. 'Verify your webhook integration(s) by checking the audit logs below for recent changes to your feature flags.'
  106. )}
  107. </TextBlock>
  108. <GridEditable
  109. error={error}
  110. isLoading={isPending}
  111. data={responseData?.data ?? []}
  112. columnOrder={columns}
  113. columnSortBy={[]}
  114. grid={{
  115. renderBodyCell,
  116. onResizeColumn: handleResizeColumn,
  117. }}
  118. onRowMouseOver={(_dataRow, key) => {
  119. setActiveRowKey(key);
  120. }}
  121. onRowMouseOut={() => {
  122. setActiveRowKey(undefined);
  123. }}
  124. highlightedRowKey={activeRowKey}
  125. scrollable={false}
  126. data-test-id="audit-log-table"
  127. />
  128. <Pagination
  129. pageLinks={pageLinks}
  130. onCursor={(cursor, path, searchQuery) => {
  131. trackAnalytics('flags.logs-paginated', {
  132. direction: cursor?.endsWith(':1') ? 'prev' : 'next',
  133. organization,
  134. surface: 'settings',
  135. });
  136. navigate({
  137. pathname: path,
  138. query: {...searchQuery, cursor},
  139. });
  140. }}
  141. />
  142. </Fragment>
  143. );
  144. }
  145. export default OrganizationFeatureFlagsAuditLogTable;